問題場景
現(xiàn)在很多工程為了功能擴(kuò)展,都給出了插件化的方式。只需要用戶配置好配置文件,提供好需要的jar包,就能完成響應(yīng)功能。本文說一下,數(shù)據(jù)轉(zhuǎn)存數(shù)的功能實現(xiàn)。現(xiàn)在項目一般都離不開數(shù)據(jù)庫,自己本身的項目就會帶這驅(qū)動包,但是也會有這樣的一種需求,就是數(shù)據(jù)額外存儲的定制化,當(dāng)產(chǎn)生的數(shù)據(jù)在自己項目的流程中不滿足現(xiàn)在使用。例如做報表,項目本身產(chǎn)生數(shù)據(jù),但是需要把里面的一部分?jǐn)?shù)據(jù)拿出來和其他文本數(shù)據(jù)結(jié)合,產(chǎn)生新的數(shù)據(jù)。或者現(xiàn)有的數(shù)據(jù)進(jìn)行時間的整合,直接變成周數(shù)據(jù),月數(shù)據(jù),年數(shù)據(jù)。這些都是需要根據(jù)不同用戶自己設(shè)定的。這些數(shù)據(jù)往往是需要另外的數(shù)據(jù)庫的,這就帶來了一個問題,項目本身有一個jar包,例如Mysql4.5的,客戶想組織信息存入mysql5.5,jar包的類是相同的,這樣就帶來了驅(qū)動加載的問題,因為類的相同的,而且驅(qū)動不是完全不兼容,而是在使用上會出問題。典型的就是ojdbc14,用這個版本的驅(qū)動建立數(shù)據(jù)庫使用語句池緩存會有問題,ojdbc5,6就沒事。(畢竟語句池緩存能帶來性能上優(yōu)化,不能說為了兼容驅(qū)動放棄性能,而且其他潛在的問題還沒有暴露)。
問題分析
本質(zhì)上就是一個獲取正確連接的問題,我們正常使用驅(qū)動第一件事就是class.forname(xxxxx),mysql(注冊肯定是需要注冊的,但是不一定發(fā)生在class.forname上)的話就是把驅(qū)動類注冊到了DriverManager上,然后我們通過DriverManager來獲取connection對象。getConnection的過程可以分為以下步驟。
1,驗證對象
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
這里的classloader的調(diào)用getConnection的對象的classloader,driver則是驅(qū)動注冊到DriverManager的對象,這里通過classloader再次加載driver類,這里如果加載到的class和driver是一樣的,當(dāng)前的classloader要不是加載驅(qū)動的classloader,要不就是其子classloader,以此保持不會出現(xiàn)類A非類A的問題(classloader的經(jīng)典問題)。
2,獲取連接
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) {
println(getConnection returning + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println( skipping: + aDriver.getClass().getName());
}
}
這里就是獲取連接的過程,registeredDrivers就是已經(jīng)注冊的驅(qū)動,它是一個集合,下面是聲明。
private final static CopyonWriteArrayList registeredDrivers = new CopyonWriteArrayList();
這樣可以看到,只要通過檢驗就獲取連接,獲取成功就返回,這就是我們獲取的connection對象。這里的一個明顯問題就是集合是一個線程安全的鏈表,誰先被加載誰先去獲取connection對象。如果按照以往的方式,這里有兩個難題,一個是classloader的驗證,一個是遍歷的順序。看起來只要讓classloader驗證正確,那么就可以保證遍歷的的時候正確的loader加載正確的驅(qū)動了。很明顯jdk不讓你這么玩,因為這個方法是private的,這就必須保證你調(diào)用的classloader和注冊的classloader必須是同一個,注冊的還簡單,但是調(diào)用的classloader也這樣,那就必須把代碼實現(xiàn)打包出來,這樣很不利于我們管理。
以上我們看通了java的連接流程,那么我們嘗試做精簡,提取關(guān)鍵代碼。
Connection con = aDriver.driver.connect(url, info);
其實獲取連接就這樣一句話,剩下的內(nèi)容都是為了保證這句話正確執(zhí)行的校驗,現(xiàn)在我們自己來保證這個校驗,這樣就把關(guān)鍵邏輯簡化成獲取Driver對象,獲取Connection對象,2個過程。無論怎么思考,都繞不過classloader,所以自定義classloader是肯定繞不過去了。我們借助classloader來保證加載的驅(qū)動類是正確的,這點就比較容易的實現(xiàn)了。
實現(xiàn)
首先需要一個違背雙親委托的classloader,不知道怎么寫的可以查看我上一篇文章。地址:https://my.oschina/xpbob/blog/761436
這種classloader很好的保證了我們加載的安全性,因為地址是我們指定的,用戶扔進(jìn)去多個相同版本那和在was的lib目錄下扔入效果一樣,都不保證正常運(yùn)行。簡單寫一個使用方式。
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
String url =jdbc:mysql://127.0.0.1/test;
String user=root;
String password =;
String driverClass=com.mysql.jdbc.Driver;
File file = new File(c:/mysql-connector-java-5.1.8.jar);
MyClassloader loader = new MyClassloader(new URL[]{file.toURL()});
Driver driver = (Driver) loader.loadClass(driverClass).newInstance();
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put(user, user);
}
if (password != null) {
info.put(password, password);
}
Connection connect = driver.connect(url, info);
if(connect!=null){
System.out.println(success);
}else{
System.out.println(error);
}
}
上面的url,username等等信息都是連接數(shù)據(jù)庫必要的信息,可以寫死也可以指定,然后那個file路徑是我指定的地址,在c盤,根據(jù)項目來設(shè)定。這里直接生成driver對象,此處就是com.mysql.jdbc.Driver,這里就體現(xiàn)到面向接口編程的好處了,Driver類是jdk的,只要保證路徑就都能加載到,不會出現(xiàn)沖突。運(yùn)行代碼,顯示success就表示你成功獲取connection對象,剩下就是操作數(shù)據(jù)庫語句的邏輯了。


