1. 程式人生 > >MySQL的JDBC驅動原始碼解析

MySQL的JDBC驅動原始碼解析

一、背景

        MySQL是一箇中小型關係型資料庫管理系統,目前我們淘寶也使用的也非常廣泛。為了對開發中間DAO持久層的問題能有更深的理解以及最近在使用的phoenix on Hbase的SQL也是實現的JDBC規範,在遇到問題的時候能夠有更多的思路,於是研究了一下MySQL_JDBC驅動的原始碼,大家都知道JDBC是Java訪問資料庫的一套規範,具體訪問資料庫的細節有各個資料庫廠商自己實現,看驅動實現也有助有我們更好的理解JDBC規範,並且在這過程中也發現了一直以來對於PreparedStatement常識理解上的錯誤,與大家分享(MySQl版本5.1.39,JDBC驅動版本5.1.7,JDK版本1.6)。  

二、JDBC典型應用
     下面是個最簡單的使用JDBC取得資料的應用。主要能分成幾個步驟,分別是①載入資料庫驅動,②獲取資料庫連線,③建立PreparedStatement,並且設定引數  ④ 執行查詢 ,來分步分析這個過程。基本上每個步驟的原始碼分析我都畫了時序圖,如果不想看文字的話,可以對著時序圖看。最後我還會分析關於PreparedStatement預編譯的話題,有興趣的同學可以仔細看下。

Java程式碼  

1. public class PreparedStatement_Select {  

2.     private Connection conn = null;  

3.     private PreparedStatement pstmt = null;  

4.     private ResultSet rs = null;  

5.     private String sql = "SELECT * FROM user WHERE id = ?";

7.     public void selectStudent(int id) {  

8.         try {  

9.             // step1:載入資料庫廠商提供的驅動程式

10.            Class.forName(“ com.mysql.jdbc.Driver

 ”);

11.        } catch (ClassNotFoundException e) {  

12.            e.printStackTrace();  

13.        }

15.        String url = "jdbc:mysql://localhost:3306/studb";  

16.        try {  

17.            // step2:提供資料庫連線的URL,通過DriverManager獲得資料庫的一個連線物件  

18.            conn = DriverManager.getConnection(url, "root""root");  

19.        } catch (SQLException e) {  

20.            e.printStackTrace();  

21.        }

23.        try {  

24.            // step3:建立Statement(SQL的執行環境)

25.            pstmt = conn.prepareStatement(sql);  

26.            pstmt.setInt(1, id);  

28.            // step4: 執行SQL語句  

29.            rs = pstmt.executeQuery();  

31.            // step5: 處理結果  

32.            while (rs.next()) {  

33.                int i = 1;  

34.                System.out.print(學員編號: " + rs.getInt(i++));  

35.                System.out.print(", 學員使用者名稱: " + rs.getString(i++));  

36.                System.out.print(", 學員密碼: " + rs.getString(i++));  

37.                System.out.println(", 學員年齡: " + rs.getInt(i++));  

38.            }  

39.        } catch (SQLException e) {  

40.            e.printStackTrace();  

41.        } finally {  

42.            // step6: 關閉資料庫連線  

43.            DbClose.close(rs, pstmt, conn);  

44.        }  

45.    }  

46.}  

三、JDBC驅動原始碼解析

Java資料庫連線(JDBC)由一組用 Java 程式語言編寫的類和介面組成。JDBC 為工具/資料庫開發人員提供了一個標準的 API,使他們能夠用純Java API 來編寫資料庫應用程式。說白了一套Java訪問資料庫的統一規範,如下圖,具體與資料庫互動的還是由驅動實現,JDBC規範之於驅動的關係,也類似於Servlet規範與Servlet容器(Tomcat)的關係,本質就是一套介面和一套實現的關係。如下類圖所示,我們平時開發JDBC時熟悉的Connection介面在Mysql驅動中的實現類是com.mysql.jdbc.JDBC4Connection類,PreparedStatement介面在Mysql驅動中的實現類是com.mysql.jdbc.JDBC4Connection, ResultSet介面在Mysql驅動中的實現類是 com.mysql.jdbc.JDBC4ResultSet,下面的原始碼解析也是通過這幾個類展開。


1:載入資料庫廠商提供的驅動程式

       首先我們通過Class.forName("com.mysql.jdbc.Driver")來載入mysql的jdbc驅動。 Mysql的com.mysql.jdbc.Driver類實現了java.sql.Driver介面,任何資料庫提供商的驅動類都必須實現這個介面。在DriverManager類中使用的都是介面Driver型別的驅動,也就是說驅動的使用不依賴於具體的實現,這無疑給我們的使用帶來很大的方便。如果需要換用其他的資料庫的話,只需要把Class.forName()中的引數換掉就可以了,可以說是非常方便的,com.mysql.jdbc.Driver類也是驅動實現JDBC規範的第一步。

Java程式碼  

1.  public class Driver extends NonRegisteringDriver implements java.sql.Driver {  

2.      static {  

3.          try { 

4.              //DriverManager中註冊自身驅動

5.              java.sql.DriverManager.registerDriver(new Driver());  

6.          } catch (SQLException E) {  

7.              throw new RuntimeException("Can't register driver!");  

8.          }  

9.      }  

10.     public Driver() throws SQLException {  

11.     }  

12.

      在com.mysql.jdbc.Driver類的靜態初始化塊中會向java.sql.DriverManager註冊它自己 ,註冊驅動首先就是初始化,然後把驅動的資訊封裝一下放進一個叫做DriverInfo的驅動資訊類中,最後放入一個驅動的集合中, 到此Mysql的驅動類com.mysql.jdbc.Driver也就已經註冊到DriverManager中了。

Java程式碼  

1.  public static synchronized void registerDriver(java.sql.Driver driver)  throws SQLException {  

2.  if (!initialized) {  

3.      initialize();  

4.  }  

6.  DriverInfo di = new DriverInfo();  

8.  //driver的資訊封裝一下,組成一個DriverInfo物件  

9.  di.driver = driver;  

10. di.driverClass = driver.getClass();  

11. di.driverClassName = di.driverClass.getName();  

13. writeDrivers.addElement(di);   

14. println("registerDriver: " + di);  

16. readDrivers = (java.util.Vector) writeDrivers.clone();  

17. }  

 註冊驅動的具體過程式列圖如下:


2.獲取資料庫連線

      資料庫連線的本質其實就是客戶端維持了一個和遠端MySQL伺服器的一個TCP長連線,並且在此連線上維護了一些資訊。

通過 DriverManager.getConnection(url, "root", "root")獲取資料庫連線物件時,由於之前已經在 DriverManager中註冊了驅動類 ,所有會找到那個驅動類來連線資料庫com.mysql.jdbc.Driver.connect

Java程式碼

1.     private static Connection getConnection(  

2.  String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {  

3.  java.util.Vector drivers = null;  

5.  if (!initialized) {  

6.      initialize();  

7.  }  

8.  //取得連線使用的driverreadDrivers中取  

9.  synchronized (DriverManager.class){   

10.     drivers = readDrivers;    

11. }  

13. SQLException reason = null;  

14. for (int i = 0; i < drivers.size(); i++) {  

15.     DriverInfo di = (DriverInfo)drivers.elementAt(i);  

17.     if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {  

18.     continue;  

19.     }  

20.     try {  

21.     // 找到可供使用的驅動,連線資料庫server  

22.     Connection result = di.driver.connect(url, info);  

23.     if (result != null) {  

24.         return (result);  

25.     }  

26.     } catch (SQLException ex) {  

27.     if (reason == null) {  

28.         reason = ex;  

29.     }  
      }  

      接著看com.mysql.jdbc.Driver.connect是如何建立連線返回資料庫連線物件的, 寫法很簡潔,就是建立了一個MySQL的資料庫連線物件, 傳入host, port, database等連線資訊,在com.mysql.jdbc.Connection的構造方法裡面有個createNewIO()方法,主要會做兩件事情,一、建立和MysqlServer的Socket連線,二、連線成功後,進行登入校驗, 傳送使用者名稱、密碼、當前資料庫連線預設選擇的資料庫名。

Java程式碼  

1.  public java.sql.Connection connect(String url, Properties info)  

2.          throws SQLException {  

3.      Properties props = null;  

4.      try { 

5.         // 傳入host,port,database等連線資訊建立資料庫連線物件 

6.          Connection newConn = new com.mysql.jdbc.ConnectionImpl(host(props),  

7.                  port(props), props, database(props), url);  

9.          return newConn;  

10.     } catch (SQLException sqlEx) {  

11.         throw sqlEx;  

12.     } catch (Exception ex) {  

13.         throw SQLError.createSQLException(...);  

14.     }  

15. }  

         繼續往下看ConnectionImpl構造器中的實現,會呼叫 createNewIO()方法來建立一個MysqlIO物件,維護在Connection中。

Java程式碼 

1.  protected ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info,  

2.          String databaseToConnectTo, String url)  

3.          throws SQLException {  

4.      try {  

5.          this.dbmd = getMetaData(falsefalse);  

6.          //建立MysqlIO物件,建立和MySql服務端的連線,並且進行登入校驗工作 

7.          createNewIO(false);  

8.          initializeStatementInterceptors();  

9.          this.io.setStatementInterceptors(this.statementInterceptors);  

10.     } catch (SQLException ex) {  

11.         cleanup(ex);  

13.         throw ex;  

14.     } 

15.     }  

16. }  

       緊接著createNewIO()會建了一個com.mysql.jdbc.MysqlIO,利用com.mysql.jdbc.StandardSocketFactory來建立一個Socket建立與MySQL伺服器的連線,然後就由這個mySqlIO來與MySql伺服器進行握手(doHandshake()),這個doHandshake主要用來初始化與MySQL server的連線,負責登陸伺服器和處理連線錯誤。在其中會分析所連線的mysql server的版本,根據不同的版本以及是否使用SSL加密資料都有不同的處理方式,並把要傳輸給資料庫server的資料都放在一個叫做packet的buffer中,呼叫send()方法往outputStream中寫入要傳送的資料。 

Java程式碼 

1.  protected void createNewIO(boolean isForReconnect)  

2.          throws SQLException {  

4.        // 建立一個MysqlIO物件,建立與Mysql伺服器的Socket連線   

5.        this.io = new MysqlIO(newHost, newPort, mergedProps, 

6.        getSocketFactoryClassName(), this,  

7.        getSocketTimeout(),  

8.         this.largeRowSizeThreshold.getValueAsInt());  

10.       // 登入校驗MySql Server, 傳送使用者名稱、密碼、當前資料庫連線預設選擇的資料庫名

11.       this.io.doHandshake(this.user, this.password,  

12.                                 this.database);

14.        // 獲取MySql資料庫連線的連線ID

15.       this.connectionId = this.io.getThreadId(); 

16.       this.isClosed = false;  

17. }

       具體的跟Mysql Server建立連線的程式碼如下:

Java程式碼 

1.  public MysqlIO(String host, int port, Properties props,  

2.       String socketFactoryClassName, ConnectionImpl conn,  

3.       int socketTimeout, int useBufferRowSizeThreshold) throws IOException, SQLException {  

4.       this.connection = conn;  

6.       try {  

7.          // 建立Socket物件,MySql伺服器建立連線  

8.          this.mysqlConnection = this.socketFactory.connect(this.host,  

9.              this.port, props);  

11.       // 獲取Socket物件  

12.       this.mysqlConnection = this.socketFactory.beforeHandshake();  

14.       //封裝SocketInputStream輸入流  

15.       if (this.connection.getUseReadAheadInput()) {  

16.         this.mysqlInput = new ReadAheadInputStream(this.mysqlConnection.getInputStream(), 16384,  

17.                 this.connection.getTraceProtocol(),  

18.                 this.connection.getLog());  

19.       } else {  

20.         this.mysqlInput = new BufferedInputStream(this.mysqlConnection.getInputStream(),  

21.                 16384);  

22.       }  

23.       //封裝ScoketOutputStream輸出流  

24.       this.mysqlOutput = new BufferedOutputStream(this.mysqlConnection.getOutputStream(),  

25.             16384);  

26. }

       具體的跟MySQL Server互動登入校驗的程式碼如下:

Java程式碼 

1.  void secureAuth411(Buffer packet, int packLength, String user,  

2.      String password, String database, boolean writeClientParams)  

3.      throws SQLException {  

5.      // 設定使用者名稱  

6.      packet.writeString(user, "utf-8"this.connection);

8.      if (password.length() != 0) {  

9.          packet.writeByte((byte0x14);  

10.         try {  

11.             // 設定密碼  

12.             packet.writeBytesNoNull(Security.scramble411(password, this.seed, this.connection));  

13.         } catch (NoSuchAlgorithmException nse) {  

14.         }   

16.     if (this.useConnectWithDb) {  

17.         // 設定連線資料庫名  

18.         packet.writeString(database, "utf-8"this.connection);  

19.     }  

21.     // Mysql伺服器傳送登入資訊包(使用者名稱、密碼、此Socket連線預設選擇的資料庫)  

22.     send(packet, packet.getPosition());

24.     byte savePacketSequence = this.packetSequence++;  

26.     // 讀取Mysql伺服器登入檢驗後傳送的狀態資訊,如果成功就返回,如果登入失敗則丟擲異常  

27.     Buffer reply = checkErrorPacket();  

28. }

            最終由SocketOutputStream經過一次RPC傳送給MySQLServer進行驗證。

Java程式碼 

1.  private final void send(Buffer packet, int packetLen)  

2.      throws SQLException {  

3.      try {  

4.              //把登入資訊的位元組流傳送給MySQL Server 

5.              this.mysqlOutput.write(packetToSend.getByteBuffer(), 0,  

6.                      packetLen);  

7.              this.mysqlOutput.flush();  

8.          }  

9.      } catch (IOException ioEx) {  

10.         throw SQLError.createCommunicationsException(this.connection,  

11.             this.lastPacketSentTimeMs, this.lastPacketReceivedTimeMs, ioEx);  

12.     }  

13. }  

具體的獲取資料庫連線的序列圖如下:


3.建立PreparedStatement,並設定引數

當建立完資料庫連線之後,就可以通過conn.prepareStatement(sql) 來獲取SQL執行環境PreparedStatement了,獲取PreparedStatement的邏輯非常簡單,會根據需要編譯的SQL語句和Connection連線物件來建立一個JDBC4PreparedStatement物件,也就是相應SQL的執行環境了,具體程式碼如下:

Java程式碼 

1.  public java.sql.PreparedStatement prepareStatement(String sql,  

2.          int resultSetType, int resultSetConcurrency) throws SQLException {  

3.      checkClosed();  

4.      PreparedStatement pStmt = null;  

6.      //需要預編譯的SQL語句  

7.      String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql): sql;  

9.      if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {  

10.         canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);  

11.     }  

12.     // 建立JDBC4PreapareStatement物件,這個SQL環境中持有預編譯SQL語句及對應的資料庫連線物件  

13.     pStmt = com.mysql.jdbc.PreparedStatement.getInstance(this, nativeSql,  

14.                 this.database);  

15.     return pStmt;  

16. }  

當建立完SQL執行環境PreparedStatement物件之後,就可以設定一些自定義的引數了,最終會把引數值儲存在JDBC4PreapareStatement的parameterValues欄位,引數型別儲存在parameterTypes中,如下程式碼:

Java程式碼 

1.  public void setInt(int parameterIndex, int x) throws SQLException {  

2.      int parameterIndexOffset = getParameterIndexOffset();  

4.      checkBounds(paramIndex, parameterIndexOffset);  

5.      byte[] parameterAsBytes = StringUtils.getBytes(String.value(x), this.charConverter,  

6.                  this.charEncoding, this.connection  

7.                          .getServerCharacterEncoding(), this.connection  

8.                          .parserKnowsUnicode());

9.      this.parameterStreams[paramIndex - 1 + parameterIndexOffset] = null;

10.     //設定引數值

11.     this.parameterValues[paramIndex - 1 + parameterIndexOffset] = parameterAsBytes;

12.     //設定引數型別

13.     this.parameterTypes[parameterIndex - 1 + getParameterIndexOffset()] = Types.INTEGER;

14. }  

  具體的建立PreparedStatement的序列圖如下:


3.執行查詢

建立完PreparedStatement之後,就一切準備就緒了,就可以通過 pstmt.executeQuery()來執行查詢了。主要思路是根據SQL模板和設定的引數,解析成一條完整的SQL語句,最後根據MySQL協議,序列化成位元組流,RPC傳送給MySQL服務端。主要的處理過程如下:

Java程式碼 

1.  public java.sql.ResultSet executeQuery() throws SQLException {  

2.      checkClosed();  

3.      ConnectionImpl locallyScopedConn = this.connection;  

4.      CachedResultSetMetaData cachedMetadata = null;  

5.      synchronized (locallyScopedConn.getMutex()) {  

6.          if (doStreaming  

7.                  && this.connection.getNetTimeoutForStreamingResults() > 0) {  

8.              locallyScopedConn.execSQL(this"SET net_write_timeout="  

9.                      + this.connection.getNetTimeoutForStreamingResults(),  

10.                     -1null, ResultSet.TYPE_FORWARD_ONLY,  

11.                     ResultSet.CONCUR_READ_ONLY, falsethis.currentCatalog,  

12.                     nullfalse);  

13.         }  

14.         //解析封裝需要傳送的sql語句,序列化成MySQL協議對應的位元組流            

15.         Buffer sendPacket = fillSendPacket();   

17.         if (locallyScopedConn.getCacheResultSetMetadata()) {  

18.             cachedMetadata = locallyScopedConn.getCachedMetaData(this.originalSql);  

19.         }  

21.         Field[] metadataFromCache = null;  

23.            // 執行sql語句,並獲取MySQL傳送過來的結果位元組流,根據MySQL協議反序列化成ResultSet

24.            this.results = executeInternal(-1, sendPacket,  

25.                     doStreaming, true,  

26.                     metadataFromCache, false);

27.   

28.         if (oldCatalog != null) {  

29.             locallyScopedConn.setCatalog(oldCatalog);  

30.         }  

32.     }  

33.     this.lastInsertId = this.results.getUpdateID();  

34.     return this.results;  

35. }  

           接下來看下 fillSendPacket() 方法怎麼來序列化成二進位制位元組流的,請看下面的程式碼分析

Java程式碼  

1.      protected Buffer fillSendPacket(byte[][] batchedParameterStrings,  

2.              InputStream[] batchedParameterStreams, boolean[] batchedIsStream,  

3.              int[] batchedStreamLengths) throws SQLException {  

4.          // connectionIO中得到傳送資料包,首先清空其中的資料  

5.          Buffer sendPacket = this.connection.getIO().getSharedSendPacket();  

6.          sendPacket.clear();

8.          //資料包的第一位為一個操作識別符號(MysqlDefs.QUERY),表示驅動向伺服器傳送的連線的操作訊號,包括有QUERY, PING, RELOAD, SHUTDOWN, PROCESS_INFO, QUIT, SLEEP等等,這個操作訊號並不是針sql語句操作而言的CRUD操作,從提供的幾種引數來看,這個操作是針對伺服器的一個操作。一般而言,使用到的都是MysqlDefs.QUERY,表示傳送的是要執行sql語句的操作。 

9.          sendPacket.writeByte((byte) MysqlDefs.QUERY);

11.         boolean useStreamLengths = this.connection  

12.                 .getUseStreamLengthsInPrepStmts();  

14.         int ensurePacketSize = 0;  

15.         for (int i = 0; i < batchedParameterStrings.length; i++) {  

16.             if (batchedIsStream[i] && useStreamLengths) {  

17.                 ensurePacketSize += batchedStreamLengths[i];  

18.             }  

19.         }  

21.         //判斷這個sendPacketbyte buffer夠不夠大,不夠大的話,按照1.25倍來擴充buffer 

22.         if (ensurePacketSize != 0) {  

23.             sendPacket.ensureCapacity(ensurePacketSize);  

24.         }  

26.         //遍歷所有的引數。在prepareStatement階段的new ParseInfo()中,驅動曾經對sql語句進行過分割,如果含有以問號標識的引數佔位符的話,就記錄下這個佔位符的位置,依據這個位置把sql分割成多段,放在了一個名為staticSql的字串中。這裡就開始把sql語句進行拼裝,把staticSqlparameterValues進行組合,放在操作符的後面

27.         for (int i = 0; i < batchedParameterStrings.length; i++) {  

28.             if ((batchedParameterStrings[i] == null)  

29.                     && (batchedParameterStreams[i] == null)) {  

30.                 throw SQLError.createSQLException(Messages  

31.                         .getString("PreparedStatement.40"//$NON-NLS-1$  

32.                         + (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS);  

33.             }  

35.            //sendPacket中加入staticSql陣列中的元素,就是分割出來的沒有”?”sql語句,並把字串轉換成byte 

36.             sendPacket.writeBytesNoNull(this.staticSqlStrings[i]);  

38.             if (batchedIsStream[i]) {  

39.                 streamToBytes(sendPacket, batchedParameterStreams[i], true,  

40.                         batchedStreamLengths[i], useStreamLengths);  

41.             } else {  

43.             //batchedParamet