轉帖自 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驅動的實現.