基于ClasspathResource路徑問題的解決
ClasspathResource路徑問題
前言
在項(xiàng)目中工程以springboot jar形式發(fā)布,跟之前容器比少了一個解壓目錄,這個過程中出現(xiàn)了ClasspathResource的文件獲取問題。具體如下:
故障情況
本地springboot工程打成jar包發(fā)布,在以下代碼r.getFile()獲取類目錄下模板Excel文件報(bào)錯:
cannot be resolved to absolute file path because it does not reside in the file system: jar

解決方案
調(diào)整代碼,直接獲取對應(yīng)的文件流,進(jìn)行封裝。

ClassPathResource詳解
ClassPathReource resource=new ClassPathResource("spring_beans.xml");
1:public class ClassPathResource extends AbstractFileResolvingResource
在ClassPathResource中,含參數(shù)String path的構(gòu)造函數(shù):
public ClassPathResource(String path ) {
this (path , (ClassLoader) null);
}
2:上述構(gòu)造函數(shù)指向了另外一個構(gòu)造函數(shù):
public ClassPathResource (String path , ClassLoader classLoader ) {
Assert. notNull(path, "Path must not be null");
String pathToUse = StringUtils.cleanPath(path);
if (pathToUse .startsWith("/")) {
pathToUse = pathToUse .substring(1);
}
this .path = pathToUse;
this .classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}
能夠看到path由StringUtils的cleanPath方法返回了pathToUse。由此,我們找到StringUtils的cleanPath方法
3:public abstract class StringUtils
public static String cleanPath (String path ) {
if (path == null) {
return null ;
}
String pathToUse = replace( path , WINDOWS_FOLDER_SEPARATOR , FOLDER_SEPARATOR);
int prefixIndex = pathToUse .indexOf(":" );
String prefix = "" ;
if (prefixIndex != -1) {
prefix = pathToUse .substring(0, prefixIndex + 1);
pathToUse = pathToUse .substring(prefixIndex + 1);
}
if (pathToUse .startsWith(FOLDER_SEPARATOR)) {
prefix = prefix + FOLDER_SEPARATOR;
pathToUse = pathToUse .substring(1);
}
String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR );
List<String> pathElements = new LinkedList<String>();
int tops = 0;
for (int i = pathArray. length - 1; i >= 0; i --) {
String element = pathArray [i ];
if (CURRENT_PATH .equals(element)) {
// Points to current directory - drop it.
}
else if (TOP_PATH.equals(element)) {
// Registering top path found.
tops ++;
}
else {
if (tops > 0) {
// Merging path element with element corresponding to top path.
tops --;
}
else {
// Normal path element found.
pathElements .add(0, element );
}
}
}
// Remaining top paths need to be retained.
for (int i = 0; i < tops; i++) {
pathElements .add(0, TOP_PATH);
}
return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR );
}
4:StringUtils類中 replace方法
public static String replace (String inString , String oldPattern , String newPattern ) {
if (!hasLength( inString ) || !hasLength(oldPattern) || newPattern == null ) {
return inString ;
}
StringBuilder sb = new StringBuilder();
int pos = 0; // our position in the old string
int index = inString .indexOf(oldPattern );
// the index of an occurrence we've found, or -1
int patLen = oldPattern.length();
while (index >= 0) {
sb.append( inString .substring(pos , index ));
sb.append( newPattern );
pos = index + patLen;
index = inString .indexOf(oldPattern, pos );
}
sb.append( inString .substring(pos ));
// remember to append any characters to the right of a match
return sb .toString();
}
5:StringUtils類類中的hasLength方法
由此可以看出,同樣的方法名,不同的方法簽名,然后在其中一個方法中引用另外一個方法。好多類都是這么用的,就像開始的時候的構(gòu)造函數(shù)那樣,雖然不知道好處 是什么,但先記下來。
public static boolean hasLength (String str ) {
return hasLength((CharSequence) str);
}
public static boolean hasLength (CharSequence str) {
return (str != null && str.length() > 0);
}
跟蹤到這里,可以知道hasLength方法的目的就是str不為空且str的長度大于0。突然發(fā)現(xiàn)CharSequence這個類沒接觸過,來看一下它的源碼
6:public interface CharSequence{}
額,源碼沒看懂 就不粘貼過來了。
7:回到StringUtils的replace方法
首先判斷傳入的三個參數(shù),如果為空后者長度小于0,直接返回inString;那么我看一下這三個參數(shù)都是什么:
inString:path 這個就是我們傳入的文件名
oldPattern:private static final String WINDOWS_FOLDER_SEPARATOR = "\\";
newPattern:private static final String FOLDER_SEPARATOR = "/" ;這兩個是文件分隔符
然后給局部變量index賦值,通過查閱API:
public int indexOf(int ch)
返回指定字符在此字符串中第一次出現(xiàn)處的索引。
意思就是在path中查找"\\",例如我寫文件的絕對路徑是D:\\文件\\API\\JDK_API_1_6_zh_CN.CHM,我就需要循環(huán)的讀取“\\”,接下來while循環(huán)中出現(xiàn)了substring方法,繼續(xù)查閱API:
public String substring(int beginIndex)
返回一個新的字符串,它是此字符串的一個子字符串。該子字符串從指定索引處的字符開始,直到此字符串末尾。
public String substring(int beginIndex,
int endIndex)
返回一個新字符串,它是此字符串的一個子字符串。該子字符串從指定的 beginIndex 處開始,直到索引 endIndex - 1 處的字符。因此,該子字符串的長度為 endIndex-beginIndex
故此 ,第一次循環(huán)會把path路徑中從0索引開始,直到第一個"\\"之間的內(nèi)容添加到StringBuffer中,然后再在StringBuffer中添加“/”,接下來pos和index都需要改變,要往后挪。因?yàn)檠h(huán)需要往后走,我們要找到第二個“\\”,覺得這個有點(diǎn)算法的意思。返回sb.toString()。
總結(jié)一下replace方法,本意是根據(jù)傳入的路徑path,如果是D:\\文件\\API\\JDK_API_1_6_zh_CN.CHM這種格式的,給轉(zhuǎn)換成D:/文件/API/JDK_API_1_6_zh_CN.CHM這種格式。
8:StringUtils的cleanPath方法:
通過replace的返回值,我們得到了可以用的路徑pathToUse,然后我們要把這個路徑下“:”給找出來,正如代碼
int prefixIndex = pathToUse.indexOf(":" );
那樣,需要知道,indexOf方法只要沒找到相應(yīng)的字符,就會返回-1,所以在下面的判斷中才會以perfixIndex是否為-1來進(jìn)行判斷。如果路 徑中有“:”,接著以D:/文件/API/JDK_API_1_6_zh_CN.CHM舉例,prefix="D:" pathToUse="/文件/API/JDK_API_1_6_zh_CN.CHM ",這個很有意思,因?yàn)槌绦虿恢牢覀冚斎氲氖墙^對路徑 帶D:的這種 ,還是/開頭的這種,或者說相對路徑,程序直接全給你判斷了。
接下來會判斷pathToUse是否以“/"開頭,是的話prefix會加上“/”,現(xiàn)在的prefix有兩種情況,可能是"D:/"這種,也可能是"/"這種,而pathToUse肯定是“文件/API/JDK_API_1_6_zh_CN.CHM ”這種了。
String[] pathArray = delimitedListToStringArray( pathToUse, FOLDER_SEPARATOR );
看到這句代碼,我估計(jì)是把pathToUse給拆成字符串?dāng)?shù)組里,就像是這樣,文件 API ***的這種。接下來看看具體的代碼是不是這樣:
9:StringUtils的delimitedListToStringArray方法
public static String[] delimitedListToStringArray(String str, String delimiter) {
return delimitedListToStringArray( str, delimiter, null );
}
public static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete ) {
if (str == null) {
return new String[0];
}
if (delimiter == null) {
return new String[] {str};
}
List<String> result = new ArrayList<String>();
if ("" .equals(delimiter)) {
for (int i = 0; i < str.length(); i++) {
result.add(deleteAny( str.substring(i , i + 1), charsToDelete));
}
}
else {
int pos = 0;
int delPos ;
while ((delPos = str.indexOf(delimiter , pos )) != -1) {
result.add(deleteAny( str.substring(pos , delPos), charsToDelete ));
pos = delPos + delimiter.length();
}
if (str .length() > 0 && pos <= str.length()) {
// Add rest of String, but not in case of empty input.
result.add(deleteAny( str.substring(pos ), charsToDelete));
}
}
return toStringArray( result);
}
先看看傳入的參數(shù):
str:pathToUse,就是文件/API/JDK_API_1_6_zh_CN.CHM
delimiter:"/"
charsToDelete:null
public static String deleteAny(String inString, String charsToDelete ) {
if (!hasLength( inString) || !hasLength(charsToDelete)) {
return inString ;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < inString.length(); i++) {
char c = inString.charAt( i);
if (charsToDelete .indexOf(c) == -1) {
sb.append( c);
}
}
return sb .toString();
}
如果說我們傳入的"/"等于""的話,顯然是不可能,我們所假如的話,會把pathTOUse倒著循環(huán),每個字符都摘出來,然后當(dāng)成字符串用,傳入deleteAny中,然后又是循環(huán),對每個字符而言,如果charsToDelete中沒有這個字符,就在StringBuilder中添加這個字符。返回值是String。當(dāng)然了,這個還沒用到。我們用到的是那個很復(fù)雜的else
我們遇到了一個循環(huán),對pathToUse而言,從索引0開始,如果pathToUse中有"/",就像文件/API/JDK_API_1_6_zh_CN.CHM 我們會得到“文件,然后還會進(jìn)入deleteAny這個方法,參數(shù)inString就是"文件",charsToDelete是null,突然發(fā)現(xiàn)charsToDelete的值為Null的話會直接返回InString,也就是“文件”。
返回到delimitedListToStringArray方法之后,接著往后循環(huán),最終的結(jié)果就是實(shí)現(xiàn)了把pathToUse給切割成若干個String的形式。
10:回到StringUtils的cleanPath方法
我們遇到了一個倒著的循環(huán),如果說我們這個是特別正常的路徑,就相當(dāng)于復(fù)制了,如果是以.或者..結(jié)尾的這些內(nèi)容,我們就把它給忽略了。
我得承認(rèn)上面這些過程真的好復(fù)雜,其實(shí)就是做了一件事,對輸入的路徑進(jìn)行了處理,只不過考慮的情況多了一點(diǎn)。所以我決定還是用debug來走一遍看看。
這時我用的是絕對路徑
終于熬到了這個方法的結(jié)束。
11:回到ClassPathResource的構(gòu)造函數(shù)
this .classLoader = ( classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
如果傳入的classLoaser有值,就返回這個值,如果沒有,就獲取一個。
public static ClassLoader getDefaultClassLoader() {
ClassLoader cl = null;
try {
cl = Thread.currentThread().getContextClassLoader();
}
catch (Throwable ex ) {
// Cannot access thread context ClassLoader - falling back...
}
if (cl == null) {
// No thread context class loader -> use class loader of this class.
cl = ClassUtils.class .getClassLoader();
if (cl == null) {
// getClassLoader() returning null indicates the bootstrap ClassLoader
try {
cl = ClassLoader.getSystemClassLoader();
}
catch (Throwable ex ) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
}
}
}
return cl ;
}
寫到這,我們的ClassPathResouce resouce實(shí)例就有了path 和 classLoader這兩個關(guān)鍵屬性。
如果我們想獲取輸入流
@Override
public InputStream getInputStream() throws IOException {
InputStream is;
if (this .clazz != null) {
is = this.clazz .getResourceAsStream(this. path);
}
else if (this.classLoader != null) {
is = this.classLoader .getResourceAsStream(this. path);
}
else {
is = ClassLoader.getSystemResourceAsStream( this.path );
}
if (is == null) {
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
return is ;
}
會判斷clazz 有沒有值,classLoader有沒有值,然后再獲取輸入流。前兩天整理了java.lang.Class這個類的意思,現(xiàn)在就能用一點(diǎn)了,先看看定義的一些屬性
private final String path ;
private ClassLoader classLoader;
private Class<?> clazz;
ClassPathResource有好幾個構(gòu)造函數(shù),有的構(gòu)造函數(shù)會傳入classLoader,有的會傳入clazz,這個clazz就是相應(yīng)的類在JVM上的實(shí)例,顯然上面的例子中并沒有這個東西,而classLoader是有的,所以通過classLoader獲取輸入流。
我覺得對ClassPathResource理解的更透徹了,雖然大部分時間都是在對path進(jìn)行處理。
近期還要看看ClassLoader,還不是很清楚它的工作機(jī)制。
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java LinkedHashMap 底層實(shí)現(xiàn)原理分析
LinkedHashMap繼承自HashMap實(shí)現(xiàn)了Map接口?;緦?shí)現(xiàn)同HashMap一樣,不同之處在于LinkedHashMap保證了迭代的有序性。其內(nèi)部維護(hù)了一個雙向鏈表,解決了 HashMap不能隨時保持遍歷順序和插入順序一致的問題。2021-05-05
Java中的類加載器_動力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java中類加載器的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
淺談SpringMVC HandlerInterceptor詭異問題排查
這篇文章主要介紹了淺談SpringMVC HandlerInterceptor詭異問題排查,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-05-05
IntelliJ IDEA創(chuàng)建maven多模塊項(xiàng)目(圖文教程)
這篇文章主要介紹了IntelliJ IDEA創(chuàng)建maven多模塊項(xiàng)目(圖文教程),非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-09-09
Mybatis關(guān)聯(lián)查詢遇到的坑-無主鍵的關(guān)聯(lián)數(shù)據(jù)去重問題
這篇文章主要介紹了Mybatis關(guān)聯(lián)查詢遇到的坑-無主鍵的關(guān)聯(lián)數(shù)據(jù)去重問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03

