mysql 驅動研究

轉帖自 http://yk94wo.blog.sohu.com/146586645.html

 

做java開發這麼久了,一直都在使用mysql,oracle的驅動,只瞭解使用
    Class.forName("com.mysql.jdbc.Driver");
    Connection con = DriverManager.getConnection(url,username,password);
卻不知道驅動程序到底爲我們做了些什麼,最近閒來無事,好好學習一下。
mysql開源,很容易就獲得了驅動的源碼,oracle的下週研究吧~~呵呵

話不多說,先貼代碼。
package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class DBHelper {
    public static Connection getConnection() {
        Connection conn = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("jdbc:mysql://localhost/ad?useUnicode=true&characterEncoding=GBK&jdbcCompliantTruncation=false",
                    "root", "root");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return conn;
    }


        /*dao中的方法*/
    public List<Adv> getAllAdvs() {
       
        Connection conn = null;
        ResultSet rs = null;
        PreparedStatement stmt = null;
        String sql = "select * from adv where id = ?";
        List<Adv> advs = new ArrayList<Adv>();

        conn = DBHelper.getConnection();
        if (conn != null) {
            try {
                stmt = conn.prepareStatement(sql);
                                stmt.setInt(1, new Integer(1));
                rs = stmt.executeQuery();

                if (rs != null) {
                    while (rs.next()) {
                        Adv adv = new Adv();
                        adv.setId(rs.getLong(1));
                        adv.setName(rs.getString(2));
                        adv.setDesc(rs.getString(3));
                        adv.setPicUrl(rs.getString(4));

                        advs.add(adv);
                    }
                }
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                try {
                    stmt.close();
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        return advs;
    }

}

1.Class.forName("com.mysql.jdbc.Driver");
 這句是使用當前類加載器去加載mysql的驅動類Driver,所有數據庫廠商的數據庫驅動類必須實現java.sql.Driver接口,mysql也不例外。
看到這裏不盡奇怪,只是加載了驅動類,但DriverManager如何知道該驅動類呢?
查看Driver類:
   public class Driver implements java.sql.Driver
{
    //
    // Register ourselves with the DriverManager
    //
   
    static
    {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    }
    catch (java.sql.SQLException E) {
        E.printStackTrace();
    }
    }
//其它無關部分省略
}
上面的紅色字體揭開了答案,原來,在類加載的使用,靜態塊被調用,驅動類Driver向DriverManager註冊了自己,所以以後就可以被使用到了,同時,我們應該注意到,這裏加載類使用的是Class.forName("");方法,它默認加載類時會調用static{}代碼塊,而ClassLoader則默認不會,切記。
查看DriverManager.registerDriver(--)方法:
 public static synchronized void registerDriver(java.sql.Driver driver)
    throws SQLException {
    if (!initialized) {
        initialize();
    }
     
    DriverInfo di = new DriverInfo();

    di.driver = driver;
    di.driverClass = driver.getClass();
    di.driverClassName = di.driverClass.getName();

    // Not Required -- drivers.addElement(di);

    writeDrivers.addElement(di);
    println("registerDriver: " + di);
   
    /* update the read copy of drivers vector */
    readDrivers = (java.util.Vector) writeDrivers.clone();

    }
wirteDrivers和readDrivers是Vector對像,用於存儲封裝好的driver類信息。

2.接下來DBHelper.java類中,
conn = DriverManager.getConnection("jdbc:mysql://localhost/ad?useUnicode=true&characterEncoding=GBK&jdbcCompliantTruncation=false", "root", "root");
那麼connection到底是如何生成的?我們一步一步看,
DriverManager:

public static Connection getConnection(String url,
    String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        // Gets the classloader of the code that called this method, may
    // be null.
    ClassLoader callerCL = DriverManager.getCallerClassLoader();

    if (user != null) {
        info.put("user", user);
    }
    if (password != null) {
        info.put("password", password);
    }

        return (getConnection(url, info, callerCL));
    }
總共有4個重載的getConnection()方法,最終都調用私有的
 private static Connection getConnection(
    String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {

    java.util.Vector drivers = null;
        /*
     * When callerCl is null, we should check the application's
     * (which is invoking this class indirectly)
     * classloader, so that the JDBC driver class outside rt.jar
     * can be loaded from here.
     */
    synchronized(DriverManager.class) {    
      // synchronize loading of the correct classloader.
      if(callerCL == null) {
          callerCL = Thread.currentThread().getContextClassLoader();
       }    
    }
     
    if(url == null) {
        throw new SQLException("The url cannot be null", "08001");
    }
   
    println("DriverManager.getConnection(/"" + url + "/")");
   
    if (!initialized) {
        initialize();
    }

    synchronized (DriverManager.class){
            // use the readcopy of drivers
        drivers = readDrivers;  
        }

    // Walk through the loaded drivers attempting to make a connection.
    // Remember the first exception that gets raised so we can reraise it.
    SQLException reason = null;
    for (int i = 0; i < drivers.size(); i++) {
        DriverInfo di = (DriverInfo)drivers.elementAt(i);
     
        // If the caller does not have permission to load the driver then
        // skip it.
        if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
        println("    skipping: " + di);
        continue;
        }
        try {
        println("    trying " + di);
        Connection result = di.driver.connect(url, info);
        if (result != null) {
            // Success!
            println("getConnection returning " + di);
            return (result);
        }
        } catch (SQLException ex) {
        if (reason == null) {
            reason = ex;
        }
        }
    }
   
    // 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");
       
}
上面紅色字體中有一段關於caller的註釋,代碼意思是如果DriverManager類的類加載器爲空的話,就使用當前線程的類加載器。仔細想想,DriverManager在rt.jar包中,它是由JDK的啓動類加載器加載的,而啓動類加載器是C編寫的,所以取得的都是空,再者,使用當前線程類加載器的話,那麼交由程序編寫者來保證能夠加載驅動類。而不至於驅動器類無法加載。非常高明的手段~!

3.上面代碼Connection result = di.driver.connect(url, info);可知,由Driver來生成Connection:
Driver:
public synchronized java.sql.Connection connect(String Url, Properties Info)
    throws java.sql.SQLException
    {
        if ((_Props = parseURL(Url, Info)) == null) {
            return null;
        }
        else {
            return new Connection (host(), port(), _Props, database(), Url, this);
        }
    }

我們來看看Conenction的構造方法:
public Connection(String Host, int port, Properties Info, String Database,
                    String Url, Driver D) throws java.sql.SQLException
  {
      if (Driver.trace) {
      Object[] Args = {Host, new Integer(port), Info,
               Database, Url, D};
      Debug.methodCall(this, "constructor", Args);
      }

      if (Host == null) {
      _Host = "localhost";
      }
      else {
      _Host = new String(Host);
      }
     
      _port = port;
     
      if (Database == null) {
      throw new SQLException("Malformed URL '" + Url + "'.", "S1000");
      }
      _Database = new String(Database);
     
      _MyURL = new String(Url);
      _MyDriver = D;
     
      String U = Info.getProperty("user");
      String P = Info.getProperty("password");
     
      if (U == null || U.equals(""))
      _User = "nobody";
      else
      _User = new String(U);
     
      if (P == null)
      _Password = "";
      else
      _Password = new String(P);
     
      // Check for driver specific properties
     
      if (Info.getProperty("autoReconnect") != null) {
      _high_availability = Info.getProperty("autoReconnect").toUpperCase().equals("TRUE");
      }
     
      if (_high_availability) {
      if (Info.getProperty("maxReconnects") != null) {
          try {
          int n = Integer.parseInt(Info.getProperty("maxReconnects"));
          _max_reconnects = n;
          }
          catch (NumberFormatException NFE) {
          throw new SQLException("Illegal parameter '" +
                     Info.getProperty("maxReconnects")
                     +"' for maxReconnects", "0S100");
          }
      }
     
      if (Info.getProperty("initialTimeout") != null) {
          try {
          double n = Integer.parseInt(Info.getProperty("intialTimeout"));
          _initial_timeout = n;
          }
          catch (NumberFormatException NFE) {
          throw new SQLException("Illegal parameter '" +
                     Info.getProperty("initialTimeout")
                     +"' for initialTimeout", "0S100");
          }
      }
      }
     
      if (Info.getProperty("maxRows") != null) {
      try {
          int n = Integer.parseInt(Info.getProperty("maxRows"));
         
          if (n == 0) {
          n = -1;
          } // adjust so that it will become MysqlDefs.MAX_ROWS
              // in execSQL()
          _max_rows = n;
      }
      catch (NumberFormatException NFE) {
          throw new SQLException("Illegal parameter '" +
                     Info.getProperty("maxRows")
                     +"' for maxRows", "0S100");
      }
      }
     
      if (Info.getProperty("useUnicode") != null) {
      String UseUnicode = Info.getProperty("useUnicode").toUpperCase();
      if (UseUnicode.startsWith("TRUE")) {
          _do_unicode = true;
      }
      if (Info.getProperty("characterEncoding") != null) {
          _Encoding = Info.getProperty("characterEncoding");
         
          // Attempt to use the encoding, and bail out if it
          // can't be used
          try {
          String TestString = "abc";
          TestString.getBytes(_Encoding);
          }
          catch (UnsupportedEncodingException UE) {
          throw new SQLException("Unsupported character encoding '" +
                     _Encoding + "'.", "0S100");
          }
      }
      }
     
      if (Driver.debug)
      System.out.println("Connect: " + _User + " to " + _Database);
      try {
      _IO = new MysqlIO(Host, port);
      _IO.init(_User, _Password);
      _IO.sendCommand(MysqlDefs.INIT_DB, _Database, null);
      _isClosed = false;
      }
      catch (java.sql.SQLException E) {
      throw E;
      }
      catch (Exception E) {
      E.printStackTrace();
      throw new java.sql.SQLException("Cannot connect to MySQL server on " + _Host + ":" + _port + ". Is there a MySQL server running on the machine/port you are trying to connect to? (" + E.getClass().getName() + ")", "08S01");
      }
  }
我們查看MysqlIO的構造方法:
 MysqlIO(String Host, int port) throws IOException, java.sql.SQLException
    {
    _port = port;
    _Host = Host;

    _Mysql_Conn = new Socket(_Host, _port);
    _Mysql_Buf_Input  = new BufferedInputStream(_Mysql_Conn.getInputStream());
    _Mysql_Buf_Output = new BufferedOutputStream(_Mysql_Conn.getOutputStream());   
    _Mysql_Input  = new DataInputStream(_Mysql_Buf_Input);
    _Mysql_Output = new DataOutputStream(_Mysql_Buf_Output);
    }

現在大家都應該清楚了,最終是創建了一個socket對象,來與DB Server交互。

3.在DBHelper.java中:PreparedStatement stmt=conn.prepareStatement(sql);
我們都知道,perpareStatement是用來預編譯sql的,可以大幅提高sql執行效率,同時避免sql注入問題 。
那麼它到底如何實現這一點的呢?
我們繼續往下看,
Connection:
public java.sql.PreparedStatement prepareStatement(String Sql) throws java.sql.SQLException
  {
      if (Driver.trace) {
      Object[] Args = {Sql};
      Debug.methodCall(this, "prepareStatement", Args);
      }
      PreparedStatement PStmt = new org.gjt.mm.mysql.PreparedStatement(this, Sql, _Database);

      if (Driver.trace) {
      Debug.returnValue(this, "prepareStatement", PStmt);
      }

    return PStmt;
  }
============================================================
PreparedStatement:
public class PreparedStatement extends org.gjt.mm.mysql.Statement
    implements java.sql.PreparedStatement 
{

    private String        _Sql              = null;
    private String[]      _TemplateStrings  = null;
    private String[]      _ParameterStrings = null;
    private InputStream[] _ParameterStreams = null;
    private boolean[]     _IsStream         = null;
    private Connection    _Conn             = null;

    private boolean       _do_concat        = false;
    private boolean       _has_limit_clause = false;
   
    /**
     * Constructor for the PreparedStatement class.
     * Split the SQL statement into segments - separated by the arguments.
     * When we rebuild the thing with the arguments, we can substitute the
     * args and join the whole thing together.
     *
     * @param conn the instanatiating connection
     * @param sql the SQL statement with ? for IN markers
     * @exception java.sql.SQLException if something bad occurs
     */

    public PreparedStatement(Connection Conn, String Sql, String Catalog) throws java.sql.SQLException
    {
    super(Conn, Catalog);

    if (Sql.indexOf("||") != -1) {
        _do_concat = true;
    }
   
    _has_limit_clause = (Sql.toUpperCase().indexOf("LIMIT") != -1);

    Vector V = new Vector();
    boolean inQuotes = false;
    int lastParmEnd = 0, i;

    _Sql = Sql;
    _Conn = Conn;

    for (i = 0; i < _Sql.length(); ++i) {
        int c = _Sql.charAt(i);
           
        if (c == '/'')
        inQuotes = !inQuotes;
        if (c == '?' && !inQuotes)
        {
            V.addElement(_Sql.substring (lastParmEnd, i));
            lastParmEnd = i + 1;
        }
    }
    V.addElement(_Sql.substring (lastParmEnd, _Sql.length()));

    _TemplateStrings = new String[V.size()];
    _ParameterStrings = new String[V.size() - 1];
    _ParameterStreams = new InputStream[V.size() - 1];
    _IsStream         = new boolean[V.size() - 1];
    clearParameters();

    for (i = 0 ; i < _TemplateStrings.length; ++i) {
        _TemplateStrings[i] = (String)V.elementAt(i);
    }

    for (int j = 0; j < _ParameterStrings.length; j++) {
        _IsStream[j] = false;
    }
    }
..............
}

注意PreparedStatement的四個成員變量,他們是現在客戶端預編譯的關鍵,注意,這裏是客戶端預編譯。
org.gjt.mm.mysql中並沒有提供接口用於使用真正意義上的服務器端預編譯。所以執行效率並和Statement差不多。
我們一般使用 PreparedStatement的sql語句入下:
select * from adv where id = ?
通過對?的定位,找出那些非字符?,即不在''中的?號,來分隔sql語句,得到sql語句數組,放在_TemplateStrings中。
當我們調用setXXX(int index, XXX xxx);時,實際上是將參數值放到_ParameterStrings中,如果是類似於流和非基本類型對象的值,則放入_ParameterStreams中,並在_IsStream中標記。

    private String[]      _TemplateStrings  = null;   //
    private String[]      _ParameterStrings = null;
    private InputStream[] _ParameterStreams = null;
    private boolean[]     _IsStream         = null;

當我們執行
ResultSet rs = stmt.executeQuery();時
實際上是將這些拼裝起來,重新生成完整的sql語句。發送到服務器端。
再次說明,org.gjt.mm.mysql並沒有實現提供接口用於使用真正的服務器端sql預編譯。
但是在後來的mysql官方驅動類中,已經實現了,我將在下一篇中詳述,其實主要是生成的sql格式和命令不一樣,就是說在發送給DB服務器的命令中明確指定需要預編譯。


這時,我們還有一個問題,就是PreparedStatement如何防止sql注入的?
很簡單,
PreparedStatement:
public void setString(int parameterIndex, String X) throws java.sql.SQLException
    {
    // if the passed string is null, then set this column to null
       
    if(X == null) {
        set(parameterIndex, "null");
    }
    else {
        StringBuffer B = new StringBuffer();
        int i;
                   
        B.append('/'');

        for (i = 0 ; i < X.length() ; ++i) {
        char c = X.charAt(i);
               
        if (c == '//' || c == '/'' || c == '"') {
            B.append((char)'//');
        }
        B.append(c);
        }
           
        B.append('/'');
        set(parameterIndex, B.toString());
    }
    }
也就是在傳進來的string 的前後強制加上了 " ' "號,明確表明這是一個string變量,也就避免了sql注入。

今天就到此爲止了,下次再詳細分析mysql官方驅動如何實現真正預編譯的,以及分析Oracle驅動的實現.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章