工作忙,有些許時間沒有更新Blog了,這次在開發(fā)監(jiān)控模塊的時候遇到了這個問題,整個問題定位過程真是走了不少路,所以覺得有必要記錄下來分享一下。在我看來很多時候結(jié)果也許就很簡單一個原因,但是開發(fā)人員卻要探究很久,也許在找到了其他可實現(xiàn)業(yè)務(wù)邏輯方法的情況下,就會放棄尋找原因,這期間我也是一樣。
問題初現(xiàn):
在服務(wù)集成平臺中需要新增一塊寫入數(shù)據(jù)庫的邏輯,因此考慮最簡便就是弄個Spring的BeanFactory來搞定這一切,誰知道,問題就這么出現(xiàn)了。很簡單,通過Spring的ClassPathXmlApplicationContext來構(gòu)建BeanFactory,下面的語句大家應(yīng)該很熟悉:
ctx = new ClassPathXmlApplicationContext("/spring/sip-*.xml");
通過通配符來載入ClassPath下的所有的符合規(guī)則的spring配置文件。然后在Eclipse中作完單元測試和集成測試,一切正常。然后用我們內(nèi)部的打包部署,而這些配置文件都被打在Jar中作為lib庫依賴。結(jié)果啟動以后,在分析完日志需要寫入到數(shù)據(jù)庫的時候出現(xiàn)異常:
Could not resolve bean definition resource pattern [/spring/sip-*.xml]; nested exception is java.io.FileNotFoundException: class path resource [spring/] cannot be resolved to URL because it does not exist
就提示來說,就是沒有找到spring這個目錄,也就是在ClassPath下面就沒有找到資源。
第一次試圖解決問題:
以前調(diào)整過Jboss關(guān)于ClassLoader的配置,即自上而下搜索還是自下而上搜索,以及是否采用Web容器的ClassLoader,開始懷疑是否是這種修改造成的問題。修改了沒有問題,然后就設(shè)置斷點跟蹤Spring的ClassPathXmlApplicationContext的構(gòu)造過程,發(fā)現(xiàn)Spring在分析此類通配類型的過程中,首先將前面的文件目錄和后面的具體通配文件分開,先定位文件目錄資源,也就是在定位文件目錄資源的過程中,找不到spring目錄,而出現(xiàn)了那個異常。看了代碼中也有對Jar的處理,但是在處理之前就出現(xiàn)了問題。
自己做了嘗試,將spring目錄和其內(nèi)容解壓到Web的Classes目錄下運(yùn)行正常,或者解壓到war下面也是正常的,這些地方其實都是ClassPath可以找到的,但是lib目錄下的jar也應(yīng)該是可以找到的。在仔細(xì)跟蹤了代碼中最后載入這些資源的ClassLoader內(nèi)的數(shù)據(jù),所有的Jar都是包含在內(nèi)的。
由于工作太多,因此將原有的打包模式作了修改,每次打包將這部分配置解壓到war下面,這樣就找到了可解決方案了,因此細(xì)致的緣由也就沒有再去追究。(如果不是后面再次遇到,這個問題就會在此了結(jié))
問題再現(xiàn):
監(jiān)控模塊中需要新增一塊寫入數(shù)據(jù)庫的邏輯,在單元測試和集成測試通過的情況下出現(xiàn)了問題,由于此次是普通的J2SE的應(yīng)用,所有的配置和依賴都打入在了Jar中,所以問題和前次一樣。
這次決定花一些時間好好找到問題所在,首先覺的Spring的資源載入應(yīng)該不會不支持從Jar中載入,這是最基本的功能,因此再次打開了Spring的源碼。
問題二次定位:
先看看ClassPathXmlApplicationContext的類圖結(jié)構(gòu):

關(guān)鍵方法就是
getResource
方法,ClassPathXmlApplicationContext
的資源定位就是采用了DefaultResourceLoader
的getResource
方法。內(nèi)部也沒有做太多的工作,其實就是如下的代碼:
try
{
// Try to parse the location as a URL...
URL url = new URL(location);
returnnew UrlResource(url);
}
catch (MalformedURLException ex)
{
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
上面的代碼都是標(biāo)準(zhǔn)的j2se的代碼.作為URL通過字符串來構(gòu)造,通常需要能夠首先獲得URL的資源全路徑,而在當(dāng)前情況下發(fā)現(xiàn)到獲取資源的時候location還是保持了spring/的狀態(tài),而沒有被替換成為所在jar的資源全路徑,那么就先作以下測試:
新建簡單的項目,然后在項目中加入包含spring配置的jar,然后作單元測試,測試代碼如下:
URL url = Thread.currentThread().getClass().getResource("/spring/");
未獲取到URL,出現(xiàn)異常。
URL url = Thread.currentThread().getClass().getResource("/spring/sip-analyzer-dataSource.xml");
正常獲取到了URL。
由此看來應(yīng)該是在獲取Jar中的目錄資源路徑的時候出現(xiàn)問題導(dǎo)致后續(xù)載入出現(xiàn)問題,嘗試直接傳入具體的文件名:
ctx = new ClassPathXmlApplicationContext("/spring/sip-analyzer-dataSource.xml");
發(fā)現(xiàn)還是出現(xiàn)問題,在new URL的時候傳入的是沒有翻譯過的文件名,考慮在傳入的過程中就直接替換成為資源路徑,因此寫了一個簡單的方法:
publicstatic String[] getRealClassPath(String[] locationfile)
{
String[] result = locationfile;
for(int i = 0 ; i < locationfile.length; i++)
{
try
{
URL url = Thread.currentThread().getClass().getResource(locationfile[i]);
String file = url.getFile();
if (file.indexOf(".jar!") > 0)
result[i] = new StringBuffer("jar:").append(file.substring(0,file.indexOf(".jar!")+".jar!".length()))
.append(locationfile[i]).toString();
}
catch(Exception ex)
{}
}
returnresult;
}
在將構(gòu)造工廠類修改為:
ctx = new ClassPathXmlApplicationContext(BaseUtil.getRealClassPath(new String[]{"/spring/sip-analyzer-dataSource.xml"}));
運(yùn)行測試,正常啟動,這也就是又變成最原始的文件羅列的模式。問題雖然找到了解決方案,但是始終覺得很別扭,同時對于無法在Jar中載入配置資源的情況我一直都還是覺得應(yīng)該不是Spring的問題。
峰回路轉(zhuǎn):
晚上到家還是有點不死心,就直接建了個項目作單元測試,然后將一個自己建立的Jar加入到Classpath下面,作單元測試,結(jié)果大吃一驚。
URL url = Thread.currentThread().getClass().getResource("/test/");
URL url = Thread.currentThread().getClass().getResource("/test/test.txt");
都正常獲取到了資源,這完全推翻了我早先認(rèn)為在Jar中無法獲得目錄作為資源的問題。然后把公司里面的項目重新打包然后加入到ClassPath下,驗證spring的目錄,出錯,目錄無法獲取,此時我確定看來應(yīng)該不是應(yīng)用的問題,而是環(huán)境問題。檢查了兩個Jar,看似沒有什么區(qū)別,將公司項目的Jar中的spring目錄拷貝到測試的jar中,然后作測試,可以找到目錄。那么問題完全定位到了Jar本身。通過RAR的壓縮工具看了一下兩個Jar的信息,除了顯示所謂的壓縮平臺不同(一個是DOS,一個是Unix)其他沒有任何區(qū)別。然后自己用RAR打了一個Jar以及在linux下打了一個Jar做了測試,兩個Jar內(nèi)的目錄都是正常可以被獲取。
無意中我換了一下需要獲取的目錄名稱,也就是說在公司項目中有多個目錄在jar中,這次換成為ibatis目錄,正常獲取,看來不是Jar的格式。回想了一下,公司的打包工具是自己人寫的,其中提供了一個特性,如果一個項目內(nèi)部的一些配置信息是需要讓調(diào)用它的第三方在編譯期配置,那么可以通過在第三方項目構(gòu)建的過程中,動態(tài)的生成配置文件然后植入到被依賴的jar中。而spring這個目錄中由于那些數(shù)據(jù)庫的配置都是需要動態(tài)配置的,因此spring的那個目錄是后期被寫入的,而ibatis是早先就固化在項目中的。
由于我們的Jar在META-INF中都有INDEX.LIST文件,過去遇到過在JAR中自己手工放入一些文件由于沒有修改INDEX.LIST而導(dǎo)致雖然文件已經(jīng)存在但是不會被發(fā)現(xiàn),于是打開公司項目中的Jar,果然INDEX.LIST中只有ibatis,而沒有spring,看來是我的同事在寫入的時候沒有將INDEX.LIST更新。立刻將INDEX.LIST作了更新,測試spring目錄,結(jié)果依然出錯。看來這還不是問題的根本。
立刻問了我們開發(fā)打包工具的同事,向他們要寫入Jar的代碼,對方的回答是就是采用簡單的JarOutputStream來寫入,沒有什么特殊的。那我就開始懷疑是否是因為采用這種方式寫入到Jar中的目錄在被資源定位的時候會出現(xiàn)問題。于是寫了下面的代碼:
JarOutputStream jos;
try
{
jos = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
String f = "spring/sip-analyzer-dataSource.xml";
File source = new File(f);
JarEntry je = new JarEntry(f);
jos.putNextEntry(je);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:/work/sip3/analyzer/src/conf.test/spring/sip-analyzer-dataSource.xml"));
int i = 0;
while ((i=bis.read())!=-1)
{
jos.write(i);
}
bis.close();
jos.closeEntry();
jos.close();
} catch ...
結(jié)果創(chuàng)建出來的Jar中的spring目錄無法被資源定位,同樣在這個Jar中直接拖入一個目錄test,然后刷新測試,test目錄可以被定位。
后續(xù)
就到了這個階段來看如果以上面這種方式寫入,對于目錄資源定位的卻存在問題。這個問題還沒有最終的肯定的結(jié)論,在我現(xiàn)在所有的試驗來看,不論是否有INDEX.LIST,或者INDEX.LIST,如果用程序?qū)懭氲?/span>Jar中,目錄作為資源定位都會出現(xiàn)問題(起碼是上面那種普通寫入方式)。
這種情況可能是由于這種寫法還有一些其他需要配置的,例如寫入到META-INF/INDEX.LIST中,或者就是J2SE現(xiàn)在API存在的一個問題。不過不管是什么問題,起碼值得引起重視,特別是現(xiàn)在類似于Spring框架載入Jar目錄下的配置。