1. Spi概述
2. 談談Sql驅動
3. 寫一個Sql驅動
4. 通過Spi機制再寫一個Sql驅動
5. 總結
-
Spi概述
Spi全稱Service Provider Interface,是一種服務發現機制,通過在指定路徑下(通常為:META-INF/services檔案夾下)讀取,讀取的內容是介面實作類,將該實作類應用起來,具體使用和操作目的在本文后續會講解,
在Spi機制中,可以看做有兩方角色:微內核+插件,對此我們可以聯想一下我們常用的開發工具Eclipse、IDEA、VScode、Atom、Sublime Text等,無一不是擁有著較大的插件生態,這些插件時如何作用起來的?
將IDE視為微內核,以IDE運行程式為例,運行時IDE可以去回呼一個介面,作為運行程式動作觸發后需要進行的操作,它可以是顯示控制臺、統計覆寫率、打開瀏覽器等,這個可以提供給插件自定義的方式,就是微內核+插件,微內核擁有著一個嚴謹的操作,負責回呼各種插件,可替換的插件有效降低了系統的耦合, -
談談Sql驅動
這會談Sql驅動,是因為Sql驅動也是一個插件,我們有各種Sql驅動JDBC、ODBC...,使用它們只需要把它們引入進來,這就已經呈現出插件可替換的特點了,
為了后面能比較順暢的進行Sql驅動的撰寫,在這先提一提DriverManager如何加載插件,
先展示下加載Sql驅動的模板陳述句:
public static void main(String[] args) {
String classPath = "org.jdbc.driver.MyDriver";
try {
Class.forName(classPath);
DriverManager.getConnection("url", "user", "pass");
// ...
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
我們看看DriverManager.getConnection()的原始碼,究竟做了什么?
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);
}
return (getConnection(url, info, Reflection.getCallerClass()));
}
經過引數檢驗后,核心是呼叫了getConnection方法,繼續看看這個方法做了什么?
代碼偏多,刪減一些注釋和陳述句
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//...刪
//這里可以看到:回圈了成員變數registeredDrivers
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());
}
}
// ...刪
}
讓我們看看這個registeredDrivers的定義
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
顯然是個陣列,那它是什么時候擁有Driver的資訊的呢?
在獲取Sql連接前,有一個需要寫并且好像并不起眼的代碼:
Class.forName(classPath);
將Driver加載了一次,而這操作就是為了加載驅動,為什么寫了這行代碼就能加載驅動?
- 寫一個Sql驅動
在DriverManager中,有一個DriverManager.registerDriver方法,用于將指定驅動加載入DriverManager.registeredDrivers驅動串列內,Class.forName(classPath)代碼會促使驅動類內靜態代碼塊的執行,所以只要在驅動類的靜態代碼塊中向DriverManager注冊我們自己撰寫的Driver,就可以成功引入我們的驅動,
public class MyDriver implements Driver {
private static Driver driver;
// 加載驅動
static {
try {
driver = new MyDriver();
// 向DriverManager注冊驅動
DriverManager.registerDriver(driver);
} catch (SQLException e) {
e.printStackTrace();
}
}
// ... 刪
}
驅動需要實作java.sql.Driver介面,在靜態代碼塊中注冊即可實作引入驅動,至于獲取連接,只需要在MyDriver.connect中回傳一個Connection,就可以在DriverManager.getConnection中獲取到該Connection,
然而,這樣的操作雖然實作了插件化(驅動就是插件),但我們每次在獲取Connection前,都需要寫一次Class.forName(classPath)的代碼來加載驅動,那么有沒有辦法能夠不用寫這行代碼呢?
- 通過Spi機制再寫一個Sql驅動
答案當然是有的,上述方式實作了插件化,但嚴格意義上不算Spi,Spi中還有一個檔案夾尚未使用,這個檔案夾到底有什么用處?
從上面的驅動加載可以知道,我們需要規避的是加載驅動的這個動作,那為了在用戶使用時規避這個動作,就需要一個管理類來做這個操作,并且該管理類需要獲取到classPath類路徑,
所以,這個檔案夾META-INF/services的意義,就是告訴讀取插件的類,需要讀取的插件的類路徑在XXX地方(充當組態檔的意義),
為了充分了解整個程序,我們談談DriverManger中使用到的插件讀取類ServiceLoader,
在DriverManager中,有這么一段靜態代碼塊:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
在loadInitialDrivers內部,有這么一段代碼:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
這個ServiceLoader充當的就是插件讀取類的角色,
回傳的loadedDrivers是一個迭代器,所以我們需要查看迭代器的方法:
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// PREFIX定義: private static final String PREFIX = "META-INF/services/";
// 此處的service是ServiceLoader<Driver>中的Driver
String fullName = PREFIX + service.getName();//看這里
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 加載對應的類(插件)
c = Class.forName(cn, false, loader);//看這里
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
有上述代碼可知,ServiceLoader在檔案夾META-INF/services/+介面全類名的檔案中讀取插件類路徑,所以利用這個Spi機制,ServiceLoader就能在迭代器獲取到類資訊,并且在nextService方法中幫助我們實作驅動類的加載,
所以對于使用Spi機制來加載驅動的插件提供者來說,只需要在META-INF/services/java.sql.Driver檔案下,寫一行插件名,
org.jdbc.driver.MyDriver
這樣DriverManager就可以通過ServiceLoader實作的Spi機制,幫助我們加載驅動,
對于Spi機制的內核方來說,就是需要實作Spi機制來加載插件提供者提供的插件,加載插件后進行什么操作,就看實際業務情況了,感興趣的小伙伴,不妨嘗試自己寫一個簡易的DriverManager以及ServiceLoader,來加載自己剛寫的Sql驅動,僅需要一點檔案操作和反射操作即可完成,
- 總結
驅動的加載只是Spi功能的冰山一角,Spi機制的存在,能讓業務代碼影響較小的情況下,靈活的替換業務流程中的各種功能,目前典型的開源專案有:Dubbo,Dubbo框架中大量應用了Spi機制,使得開發者能夠輕松替換Dubbo中的各種功能,例如日志、rpc訪問等,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/286537.html
標籤:Java
上一篇:Python return邏輯判斷運算式 - Python零基礎入門教程
下一篇:Web 基礎——Tomcat
