JDBC是一套鏈接和操做數據庫的標準、規範。經過提供DriverManager
、Connection
、Statement
、ResultSet
等接口將開發人員與數據庫提供商隔離,開發人員只須要面對JDBC接口,無需關心怎麼跟數據庫交互。java
類名 | 做用 |
---|---|
DriverManager |
驅動管理器,用於註冊驅動,是獲取 Connection 對象的入口 |
Driver |
數據庫驅動,用於獲取Connection 對象 |
Connection |
數據庫鏈接,用於獲取Statement 對象、管理事務 |
Statement |
sql執行器,用於執行sql |
ResultSet |
結果集,用於封裝和操做查詢結果 |
prepareCall |
用於調用存儲過程 |
記得釋放資源。另外,ResultSet
和Statement
的關閉都不會致使Connection
的關閉。mysql
maven要引入oracle的驅動包,要把jar包安裝在本地倉庫或私服才行。git
使用PreparedStatement
而不是Statement
。能夠避免SQL注入,而且利用預編譯的特色能夠提升效率。github
使用JDBC對mysql數據庫的用戶表進行增刪改查。sql
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
,須要將驅動包手動添加到本地倉庫或私服。
下面的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
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; }
這裏簡單地模擬實際業務層調用持久層,並開啓事務。另外,獲取鏈接、開啓事務、提交回滾、釋放資源都經過自定義的工具類 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
主要用於管理數據庫驅動,併爲咱們提供了獲取鏈接對象的接口。其中,它有一個重要的成員屬性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; } }
當加載com.mysql.cj.jdbc.Driver
這個類時,靜態代碼塊中會執行註冊驅動的方法。
static { try { //靜態代碼塊中註冊當前驅動 java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } }
由於從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
,調用時須要傳入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()); } } }
因爲使用的是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; } }
這個類有個比較重要的字段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; }
接下來的代碼主要是創建會話的過程,首先時創建物理鏈接,而後根據協議創建會話。
注意:考慮篇幅,如下代碼通過修改,僅保留所需部分。
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