一、概述
本文旨在傳遞更多JDBC的細節和原理。環境是Mysql,後續會繼續介紹事務、數據源和獲取數據庫元信息。
本文涉及問題概覽:
1. Class.forName這句代碼做了什麼?
2. Class.forName和ClassLoader.loadClass區別?
3. DriverManager介紹;
二、詳解
簡單的JDBC操作,有以下幾個步驟:
1. 註冊驅動;
2. 獲取鏈接;
3. 創建執行對象;
4. 執行SQL語句獲得結果集;
5. 處理結果集;
6. 關閉資源;
一個完整的小例子,旨在引出下文。
package cn.wxy.jdbc;
import java.io.Serializable;
/**
* domain對象,用於封裝數據
* @author wxy51
*/
public class User implements Serializable {
private static final long serialVersionUID = 7823619631737327735L;
private Integer id;
private String username;
private String password;
private String gender;
public User() {
}
public User(Integer id, String username, String password, String gender) {
super();
this.id = id;
this.username = username;
this.password = password;
this.gender = gender;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String toString() {
return "User [id=" + id + ", username=" + username + ", password="
+ password + ", gender=" + gender + "]";
}
}
package cn.wxy.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class SimpleJdbcOperation {
/**
* jdbc查詢簡單示例
* @param id 用戶id
* @return 數據庫中該用戶id對應記錄
*/
public User queryOneUser(Integer id) {
if(id == null)
return null;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rSet = null;
User user = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager
.getConnection("jdbc:mysql://localhost:3306/demo", "用戶名",
"密碼");
pstmt = conn.prepareStatement("select * from user where id=?");
pstmt.setInt(1, id);
rSet = pstmt.executeQuery();
if (rSet.next())
user = new User(rSet.getInt(1), rSet.getString(2),
rSet.getString(3), rSet.getString(4));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null)
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
if (pstmt != null)
try {
pstmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
if (rSet != null)
try {
rSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
conn = null;
pstmt = null;
rSet = null;
}
return user;
}
}
1. 註冊驅動
在JDK API中,註冊驅動的方式(具體在java.sql.Driver接口的API中)已經明確指出“When a Driver class is loaded, it should create an instance of itself and register it with the DriverManager. This means that a user can load and register a driver by calling Class.forName(your driver path)”。
以mysql環境爲例,推薦的驅動註冊方式爲Class.forName("com.mysql.jdbc.Driver");,參數“com.mysql.jdbc.Driver”是mysql jdbc驅動包中實現了java.sql.Driver接口規範的驅動實現類,先不看com.mysql.jdbc.Driver的源碼,Class.forName這句代碼做了什麼?
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
private static native Class<?> forName0(String name, boolean initialize, ClassLoader loader,
Class<?> caller) throws ClassNotFoundException;
查看Class的源碼,這句話是根據一個類的全量路徑獲得這個類的Class對象,但Class.forName這句代碼所做的事情絕不僅僅只是獲取Class對象這麼一點。
類加載器是JDK中一個模塊,這個模塊描述的是將一個類的字節碼文件加載到內存,併爲之生成java.lang.Class對象作爲其對外訪問的接口,他涉及三個環節五個步驟,加載、連接(驗證、準備、解析)和初始化,java的初始化知識涉及類初始化和對象初始化,那麼在類加載過程中,準備和初始化兩個步驟則描述了類初始化的過程中static修飾的變量和代碼塊的默認初始化和語句執行,其實也就是對應着生成類構造器<clinit>和執行類構造器的過程,從代碼層面上來講,就是static修飾的屬性賦值語句和靜態代碼塊中的語句將會被執行。
和類加載器ClassLoader的loadClass相同的是,這兩個方法都是根據一個類的全量路徑加載這個類的字節碼並生成java.lang.Class對象返回,但是loadClass只涉及了五個步驟中的第一個“加載”,而Class.forName則完整的經歷了五個步驟(可嘗試用兩種方式去加載一個含有靜態代碼塊的類,你會發現loadCalss的加載方式將不會執行靜態代碼塊中的內容),也就意味着loadClass不會使得類中的靜態屬性或靜態代碼塊的語句被執行。
鋪墊夠了,那麼回過頭來看com.mysql.jdbc.Driver的源碼,除了一個空構造方法之外,就只有一個靜態代碼塊,代碼塊的內容就是註冊驅動及其過程。而根據JDBC的規範,代碼Class.forName("com.mysql.jdbc.Driver")語義將會使得這個靜態代碼塊被執行。
package com.mysql.jdbc;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
因此可以得出結論:
1. Class.forName這句代碼通過java類加載的知識可知其實現了驅動註冊,而根據JDBC規範可知,數據庫廠商如果支持JDBC技術,那麼無論是mysql還是oracle等等廠商,都可以通過Class.forName來實現驅動的註冊;因此可以預見的是,實現JDBC的驅動包中,幾乎都會有這樣的代碼出現;
2. 不應該通過DriverManager.registerDriver(new com.mysql.jdbc.Driver())這樣的代碼來實現驅動註冊,通過對象實例化的知識可知這樣會導致驅動重複註冊;
3. 是否會導致同一個數據庫被重複註冊多個驅動?答案是不會的,因爲static代碼塊只會被執行一次,如果要實現連接池一類的引用,那麼應該顯式的調用DriverManager.registerDriver去註冊多個驅動;
2. DriverManager
java.sql.DriverManager是JDBC中提供的少有的不是接口或抽象類的類,它裏面全是靜態方法,是JDBC規範中對外提供的一個工具類,用來註冊驅動以及管理成功註冊的驅動,還有就是從驅動獲取數據庫連接。驅動源碼下一小結在看,先看看註冊驅動的過程。
private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
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 + "\")");
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// 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");
}
}
既然是通過驅動來獲取連接對象,那麼首先要清楚的問題是如何從ArrayList中獲取正確的驅動對象。源碼已經透露很多信息了,通過isDriverAllowed方法來判定該驅動是否適配改數據庫,其實就是遍歷存放已註冊驅動的ArrayList,return第一個適配的驅動,通過比較該結構中的每一個驅動對象的Class和類加載器對象是否相同,如果相同則表示該驅動適配改數據庫,接着就進行獲取鏈接。
3. 獲得連接
未完待續
三、補充
持久層技術,JDBC只是開篇,後續進階apache commons DButils輕量級JDBC框架,再進一步spring JDBC,屆時將接觸spring獨家的事務的傳播特性,最後是Mybatis,不完全ORM的持久層框架,最後面是逐漸淪爲雞肋的完全ORM框架hibernate,至於hibernate,有時間就看吧,依然雞肋。
但只看JDBC,除了基本操作之外,還有事務和數據源已經獲取數據庫元信息,本文只介紹了基本操作,後續還會繼續講解其他部分。