SPI機制為很多框架的擴展提供了可能,其實JDBC就應用到了這一機制。回憶一下JDBC獲取數據庫連接的過程。在早期版本中,需要先設置數據庫驅動的連接,再通過DriverManager.getConnection獲取一個Connection。
String url = "jdbc:mysql:///consult?serverTimezone=UTC"; String user = "root"; String password = "root"; Class.forName("com.mysql.jdbc.Driver"); Connection connection = DriverManager.getConnection(url, user, password);
在較新版本中(具體哪個版本,筆者沒有驗證),設置數據庫驅動連接,這一步驟就不再需要,那么它是怎么分辨是哪種數據庫的呢?答案就在SPI。
1、加載
我們把目光回到DriverManager類,它在靜態代碼塊里面做了一件比較重要的事。很明顯,它已經通過SPI機制, 把數據庫驅動連接初始化了。
public class DriverManager { static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } }
具體過程還得看loadInitialDrivers,它在里面查找的是Driver接口的服務類,所以它的文件路徑就是:META-INF/services/java.sql.Driver。
public class DriverManager { private static void loadInitialDrivers() { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { //很明顯,它要加載Driver接口的服務類,Driver接口的包為:java.sql.Driver //所以它要找的就是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; } }); } }
那么,這個文件哪里有呢?我們來看MySQL的jar包,就是這個文件,文件內容為:com.mysql.cj.jdbc.Driver。
2、創建實例
上一步已經找到了MySQL中的com.mysql.cj.jdbc.Driver全限定類名,當調用next方法時,就會創建這個類的實例。它就完成了一件事,向DriverManager注冊自身的實例。
public class Driver extends NonRegisteringDriver implements java.sql.Driver { static { try { //注冊 //調用DriverManager類的注冊方法 //往registeredDrivers集合中加入實例 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() } }
3、創建Connection
在DriverManager.getConnection()方法就是創建連接的地方,它通過循環已注冊的數據庫驅動程序,調用其connect方法,獲取連接並返回。
private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { //registeredDrivers中就包含com.mysql.cj.jdbc.Driver實例 for(DriverInfo aDriver : registeredDrivers) { if(isDriverAllowed(aDriver.driver, callerCL)) { try { //調用connect方法創建連接 Connection con = aDriver.driver.connect(url, info); if (con != null) { return (con); } }catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } } }
4、再擴展
既然我們知道JDBC是這樣創建數據庫連接的,我們能不能再擴展一下呢?如果我們自己也創建一個java.sql.Driver文件,自定義實現類MyDriver,那么,在獲取連接的前后就可以動態修改一些信息。
還是先在項目ClassPath下創建文件,文件內容為自定義驅動類com.viewscenes.netsupervisor.spi.MyDriver
我們的MyDriver實現類,繼承自MySQL中的NonRegisteringDriver,還要實現java.sql.Driver接口。這樣,在調用connect方法的時候,就會調用到此類,但實際創建的過程還靠MySQL完成。
package com.viewscenes.netsupervisor.spi public class MyDriver extends NonRegisteringDriver implements Driver{ static { try { java.sql.DriverManager.registerDriver(new MyDriver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } public MyDriver()throws SQLException {} public Connection connect(String url, Properties info) throws SQLException { System.out.println("准備創建數據庫連接.url:"+url); System.out.println("JDBC配置信息:"+info); info.setProperty("user", "root"); Connection connection = super.connect(url, info); System.out.println("數據庫連接創建完成!"+connection.toString()); return connection; } } --------------------輸出結果--------------------- 准備創建數據庫連接.url:jdbc:mysql:///consult?serverTimezone=UTC JDBC配置信息:{user=root, password=root} 數據庫連接創建完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f
