jdbc-mysql測試例子和原始碼詳解
目錄
- 簡介
- 什麼是JDBC
- 幾個重要的類
- 使用中的注意事項
- 使用例子
- 需求
- 工程環境
- 主要步驟
- 建立表
- 建立專案
- 引入依賴
- 編寫jdbc.prperties
- 獲得Connection物件
- 使用Connection物件完成儲存操作
- 原始碼分析
- 驅動註冊
- DriverManager.registerDriver
- 為什麼Class.forName(com.mysql.cj.jdbc.Driver) 可以註冊驅動?
- 為什麼JDK6後不需要Class.forName也能註冊驅動?
- 獲得連線物件
- DriverManager.getConnection
- com.mysql.cj.jdbc.Driver.connection
- ConnectionImpl.getInstance
- NativeSession.connect
- 驅動註冊
簡介
什麼是JDBC
JDBC是一套連線和操作資料庫的標準、規範。通過提供DriverManager
、Connection
、Statement
、ResultSet
等介面將開發人員與資料庫提供商隔離,開發人員只需要面對JDBC介面,無需關心怎麼跟資料庫互動。
幾個重要的類
類名 | 作用 |
---|---|
DriverManager |
驅動管理器,用於註冊驅動,是獲取 Connection 物件的入口 |
Driver |
資料庫驅動,用於獲取Connection 物件 |
Connection |
資料庫連線,用於獲取Statement |
Statement |
sql執行器,用於執行sql |
ResultSet |
結果集,用於封裝和操作查詢結果 |
prepareCall |
用於呼叫儲存過程 |
使用中的注意事項
記得釋放資源。另外,
ResultSet
和Statement
的關閉都不會導致Connection
的關閉。maven要引入oracle的驅動包,要把jar包安裝在本地倉庫或私服才行。
使用
PreparedStatement
而不是Statement
。可以避免SQL注入,並且利用預編譯的特點可以提高效率。
使用例子
需求
使用JDBC對mysql資料庫的使用者表進行增刪改查。
工程環境
JDK:1.8
maven:3.6.1
IDE:sts4
mysql driver:8.0.15
mysql:5.7
主要步驟
一個完整的JDBC儲存操作主要包括以下步驟:
註冊驅動(JDK6後會自動註冊,可忽略該步驟);
通過
DriverManager
獲得Connection
物件;開啟事務;
通過
Connection
獲得PreparedStatement
物件;設定
PreparedStatement
的引數;執行儲存操作;
儲存成功提交事務,儲存失敗回滾事務;
釋放資源,包括
Connection
、PreparedStatement
。
建立表
CREATE TABLE `demo_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '使用者id',
`name` varchar(16) COLLATE utf8_unicode_ci NOT NULL COMMENT '使用者名稱',
`age` int(3) unsigned DEFAULT NULL COMMENT '使用者年齡',
`gmt_create` datetime DEFAULT NULL COMMENT '記錄建立時間',
`gmt_modified` datetime DEFAULT NULL COMMENT '記錄最近修改時間',
`deleted` bit(1) DEFAULT b'0' COMMENT '是否刪除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`),
KEY `index_age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
建立專案
專案型別Maven Project,打包方式jar
引入依賴
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- mysql驅動的jar包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<!-- oracle驅動的jar包 -->
<!-- <dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.2.0</version>
</dependency> -->
注意:由於oracle商業版權問題,maven並不提供Oracle JDBC driver
,需要將驅動包手動新增到本地倉庫或私服。
編寫jdbc.prperties
下面的url拼接了好幾個引數,主要為了避免亂碼和時區報錯的異常。
路徑:resources目錄下
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
#這裡指定了字元編碼和解碼格式,時區,是否加密傳輸
username=root
password=root
#注意,xml配置是&採用&替代
如果是oracle資料庫,配置如下:
driver=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@//localhost:1521/xe
username=system
password=root
獲得Connection物件
private static Connection createConnection() throws Exception {
// 匯入配置檔案
Properties pro = new Properties();
InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream( "jdbc.properties" );
Connection conn = null;
pro.load( in );
// 獲取配置檔案的資訊
String driver = pro.getProperty( "driver" );
String url = pro.getProperty( "url" );
String username = pro.getProperty( "username" );
String password = pro.getProperty( "password" );
// 註冊驅動,JDK6後不需要再手動註冊,DirverManager的靜態程式碼塊會幫我們註冊
// Class.forName(driver);
// 獲得連線
conn = DriverManager.getConnection( url, username, password );
return conn;
}
使用Connection物件完成儲存操作
這裡簡單地模擬實際業務層呼叫持久層,並開啟事務。另外,獲取連線、開啟事務、提交回滾、釋放資源都通過自定義的工具類 JDBCUtil
來實現,具體見原始碼。
@Test
public void save() {
UserDao userDao = new UserDaoImpl();
// 建立使用者
User user = new User( "zzf002", 18, new Date(), new Date() );
try {
// 開啟事務
JDBCUtil.startTrasaction();
// 儲存使用者
userDao.insert( user );
// 提交事務
JDBCUtil.commit();
} catch( Exception e ) {
// 回滾事務
JDBCUtil.rollback();
e.printStackTrace();
} finally {
// 釋放資源
JDBCUtil.release();
}
}
接下來看看具體的儲存操作,即DAO層方法。
public void insert( User user ) throws Exception {
String sql = "insert into demo_user (name,age,gmt_create,gmt_modified) values(?,?,?,?)";
Connection connection = JDBCUtil.getConnection();
//獲取PreparedStatement物件
PreparedStatement prepareStatement = connection.prepareStatement( sql );
//設定引數
prepareStatement.setString( 1, user.getName() );
prepareStatement.setInt( 2, user.getAge() );
prepareStatement.setDate( 3, new java.sql.Date( user.getGmt_create().getTime() ) );
prepareStatement.setDate( 4, new java.sql.Date( user.getGmt_modified().getTime() ) );
//執行儲存
prepareStatement.executeUpdate();
//釋放資源
JDBCUtil.release( prepareStatement, null );
}
原始碼分析
驅動註冊
DriverManager.registerDriver
DriverManager
主要用於管理資料庫驅動,併為我們提供了獲取連線物件的介面。其中,它有一個重要的成員屬性registeredDrivers
,是一個CopyOnWriteArrayList
集合(通過ReentrantLock
實現執行緒安全),存放的是元素是DriverInfo
物件。
//存放資料庫驅動包裝類的集合(執行緒安全)
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
public static synchronized void registerDriver(java.sql.Driver driver)
throws SQLException {
//呼叫過載方法,傳入的DriverAction物件為null
registerDriver(driver, null);
}
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
if(driver != null) {
//當列表中沒有這個DriverInfo物件時,加入列表。
//注意,這裡判斷物件是否已經存在,最終比較的是driver地址是否相等。
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
為什麼集合存放的是Driver
的包裝類DriverInfo
物件,而不是Driver
物件呢?
通過
DriverInfo
的原始碼可知,當我們呼叫equals
方法比較兩個DriverInfo
物件是否相等時,實際上比較的是Driver
物件的地址,也就是說,我可以在DriverManager
中註冊多個MYSQL驅動。而如果直接存放的是Driver
物件,就不能達到這種效果(因為沒有遇到需要註冊多個同類驅動的場景,所以我暫時理解不了這樣做的好處)。DriverInfo
中還包含了另一個成員屬性DriverAction
,當我們登出驅動時,必須呼叫它的deregister
方法後才能將驅動從註冊列表中移除,該方法決定登出驅動時應該如何處理活動連線等(其實一般在構造DriverInfo
進行註冊時,傳入的DriverAction
物件為空,根本不會去使用到這個物件,除非一開始註冊就傳入非空DriverAction
物件)。
綜上,集合中元素不是Driver
物件而DriverInfo
物件,主要考慮的是擴充套件某些功能,雖然這些功能幾乎不會用到。
注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。
class DriverInfo {
final Driver driver;
DriverAction da;
DriverInfo(Driver driver, DriverAction action) {
this.driver = driver;
da = action;
}
@Override
public boolean equals(Object other) {
//這裡對比的是地址
return (other instanceof DriverInfo)
&& this.driver == ((DriverInfo) other).driver;
}
}
為什麼Class.forName(com.mysql.cj.jdbc.Driver) 可以註冊驅動?
當載入com.mysql.cj.jdbc.Driver
這個類時,靜態程式碼塊中會執行註冊驅動的方法。
static {
try {
//靜態程式碼塊中註冊當前驅動
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
為什麼JDK6後不需要Class.forName也能註冊驅動?
因為從JDK6開始,DriverManager
增加了以下靜態程式碼塊,當類被載入時會執行static程式碼塊的loadInitialDrivers
方法。
而這個方法會通過查詢系統引數(jdbc.drivers)和SPI機制兩種方式去載入資料庫驅動。
注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。
static {
loadInitialDrivers();
}
//這個方法通過兩個渠道載入所有資料庫驅動:
//1. 查詢系統引數jdbc.drivers獲得資料驅動類名
//2. SPI機制
private static void loadInitialDrivers() {
//通過系統引數jdbc.drivers讀取資料庫驅動的全路徑名。該引數可以通過啟動引數來設定,其實引入SPI機制後這一步好像沒什麼意義了。
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
//使用SPI機制載入驅動
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//讀取META-INF/services/java.sql.Driver檔案的類全路徑名。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//載入並初始化類
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
if (drivers == null || drivers.equals("")) {
return;
}
//載入jdbc.drivers引數配置的實現類
String[] driversList = drivers.split(":");
for (String aDriver : driversList) {
try {
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
補充:SPI機制本質上提供了一種服務發現機制,通過配置檔案的方式,實現服務的自動裝載,有利於解耦和麵向介面程式設計。具體實現過程為:在專案的META-INF/services
資料夾下放入以介面全路徑名命名的檔案,並在檔案中加入實現類的全限定名,接著就可以通過ServiceLoder
動態地載入實現類。
開啟mysql的驅動包就可以看到一個java.sql.Driver
檔案,裡面就是mysql驅動的全路徑名。
獲得連線物件
DriverManager.getConnection
獲取連線物件的入口是DriverManager.getConnection
,呼叫時需要傳入url、username和password。
獲取連線物件需要呼叫java.sql.Driver
實現類(即資料庫驅動)的方法,而具體呼叫哪個實現類呢?
正如前面講到的,註冊的資料庫驅動被存放在registeredDrivers
中,所以只有從這個集合中獲取就可以了。
注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。
public static Connection getConnection(String url, String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
//傳入url、包含username和password的資訊類、當前呼叫類
return (getConnection(url, info, Reflection.getCallerClass()));
}
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
//遍歷所有註冊的資料庫驅動
for(DriverInfo aDriver : registeredDrivers) {
//先檢查這當前類載入器是否有許可權載入這個驅動,如果是才進入
if(isDriverAllowed(aDriver.driver, callerCL)) {
//這一步是關鍵,會去呼叫Driver的connect方法
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return con;
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
}
com.mysql.cj.jdbc.Driver.connection
由於使用的是mysql的資料驅動,這裡實際呼叫的是com.mysql.cj.jdbc.Driver
的方法。
從以下程式碼可以看出,mysql支援支援多節點部署的策略,本文僅對單機版進行擴充套件。
注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。
//mysql支援多節點部署的策略,根據架構不同,url格式也有所區別。
private static final String REPLICATION_URL_PREFIX = "jdbc:mysql:replication://";
private static final String URL_PREFIX = "jdbc:mysql://";
private static final String MXJ_URL_PREFIX = "jdbc:mysql:mxj://";
public static final String LOADBALANCE_URL_PREFIX = "jdbc:mysql:loadbalance://";
public java.sql.Connection connect(String url, Properties info) throws SQLException {
//根據url的型別來返回不同的連線物件,這裡僅考慮單機版
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
switch (conStr.getType()) {
case SINGLE_CONNECTION:
//呼叫ConnectionImpl.getInstance獲取連線物件
return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());
case LOADBALANCE_CONNECTION:
return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl) conStr);
case FAILOVER_CONNECTION:
return FailoverConnectionProxy.createProxyInstance(conStr);
case REPLICATION_CONNECTION:
return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl) conStr);
default:
return null;
}
}
ConnectionImpl.getInstance
這個類有個比較重要的欄位session
,可以把它看成一個會話,和我們平時瀏覽器訪問伺服器的會話差不多,後續我們進行資料庫操作就是基於這個會話來實現的。
注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。
private NativeSession session = null;
public static JdbcConnection getInstance(HostInfo hostInfo) throws SQLException {
//呼叫構造
return new ConnectionImpl(hostInfo);
}
public ConnectionImpl(HostInfo hostInfo) throws SQLException {
//先根據hostInfo初始化成員屬性,包括資料庫主機名、埠、使用者名稱、密碼、資料庫及其他引數設定等等,這裡省略不放入。
//最主要看下這句程式碼
createNewIO(false);
}
public void createNewIO(boolean isForReconnect) {
if (!this.autoReconnect.getValue()) {
//這裡只看不重試的方法
connectOneTryOnly(isForReconnect);
return;
}
connectWithRetries(isForReconnect);
}
private void connectOneTryOnly(boolean isForReconnect) throws SQLException {
JdbcConnection c = getProxy();
//呼叫NativeSession物件的connect方法建立和資料庫的連線
this.session.connect(this.origHostInfo, this.user, this.password, this.database, DriverManager.getLoginTimeout() * 1000, c);
return;
}
NativeSession.connect
接下來的程式碼主要是建立會話的過程,首先時建立物理連線,然後根據協議建立會話。
注意:考慮篇幅,以下程式碼經過修改,僅保留所需部分。
public void connect(HostInfo hi, String user, String password, String database, int loginTimeout, TransactionEventHandler transactionManager)
throws IOException {
//首先獲得TCP/IP連線
SocketConnection socketConnection = new NativeSocketConnection();
socketConnection.connect(this.hostInfo.getHost(), this.hostInfo.getPort(), this.propertySet, getExceptionInterceptor(), this.log, loginTimeout);
// 對TCP/IP連線進行協議包裝
if (this.protocol == null) {
this.protocol = NativeProtocol.getInstance(this, socketConnection, this.propertySet, this.log, transactionManager);
} else {
this.protocol.init(this, socketConnection, this.propertySet, transactionManager);
}
// 通過使用者名稱和密碼連線指定資料庫,並建立會話
this.protocol.connect(user, password, database);
}
針對資料庫的連線,暫時點到為止,另外還有涉及資料庫操作的原始碼分析,後續再完善補充。
本文為原創文章,轉載請附上原文出處連結:https://github.com/ZhangZiSheng001/jdbc-demo