基於ClasspathResource路徑問題的解決
ClasspathResource路徑問題
前言
在項目中工程以springboot jar形式發佈,跟之前容器比少瞭一個解壓目錄,這個過程中出現瞭ClasspathResource的文件獲取問題。具體如下:
故障情況
本地springboot工程打成jar包發佈,在以下代碼r.getFile()獲取類目錄下模板Excel文件報錯:
cannot be resolved to absolute file path because it does not reside in the file system: jar
解決方案
調整代碼,直接獲取對應的文件流,進行封裝。
ClassPathResource詳解
ClassPathReource resource=new ClassPathResource("spring_beans.xml");
1:public class ClassPathResource extends AbstractFileResolvingResource
在ClassPathResource中,含參數String path的構造函數:
public ClassPathResource(String path ) { this (path , (ClassLoader) null); }
2:上述構造函數指向瞭另外一個構造函數:
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方法
由此可以看出,同樣的方法名,不同的方法簽名,然後在其中一個方法中引用另外一個方法。好多類都是這麼用的,就像開始的時候的構造函數那樣,雖然不知道好處 是什麼,但先記下來。
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。突然發現CharSequence這個類沒接觸過,來看一下它的源碼
6:public interface CharSequence{}
額,源碼沒看懂 就不粘貼過來瞭。
7:回到StringUtils的replace方法
首先判斷傳入的三個參數,如果為空後者長度小於0,直接返回inString;那麼我看一下這三個參數都是什麼:
inString:path 這個就是我們傳入的文件名
oldPattern:private static final String WINDOWS_FOLDER_SEPARATOR = “\\”;
newPattern:private static final String FOLDER_SEPARATOR = “/” ;這兩個是文件分隔符
然後給局部變量index賦值,通過查閱API:
public int indexOf(int ch)
返回指定字符在此字符串中第一次出現處的索引。
意思就是在path中查找”\\”,例如我寫文件的絕對路徑是D:\\文件\\API\\JDK_API_1_6_zh_CN.CHM,我就需要循環的讀取“\\”,接下來while循環中出現瞭substring方法,繼續查閱API:
public String substring(int beginIndex)
返回一個新的字符串,它是此字符串的一個子字符串。該子字符串從指定索引處的字符開始,直到此字符串末尾。
public String substring(int beginIndex, int endIndex)
返回一個新字符串,它是此字符串的一個子字符串。該子字符串從指定的 beginIndex 處開始,直到索引 endIndex – 1 處的字符。因此,該子字符串的長度為 endIndex-beginIndex
故此 ,第一次循環會把path路徑中從0索引開始,直到第一個”\\”之間的內容添加到StringBuffer中,然後再在StringBuffer中添加“/”,接下來pos和index都需要改變,要往後挪。因為循環需要往後走,我們要找到第二個“\\”,覺得這個有點算法的意思。返回sb.toString()。
總結一下replace方法,本意是根據傳入的路徑path,如果是D:\\文件\\API\\JDK_API_1_6_zh_CN.CHM這種格式的,給轉換成D:/文件/API/JDK_API_1_6_zh_CN.CHM這種格式。
8:StringUtils的cleanPath方法:
通過replace的返回值,我們得到瞭可以用的路徑pathToUse,然後我們要把這個路徑下“:”給找出來,正如代碼
int prefixIndex = pathToUse.indexOf(":" );
那樣,需要知道,indexOf方法隻要沒找到相應的字符,就會返回-1,所以在下面的判斷中才會以perfixIndex是否為-1來進行判斷。如果路 徑中有“:”,接著以D:/文件/API/JDK_API_1_6_zh_CN.CHM舉例,prefix=”D:” pathToUse=”/文件/API/JDK_API_1_6_zh_CN.CHM “,這個很有意思,因為程序不知道我們輸入的是絕對路徑 帶D:的這種 ,還是/開頭的這種,或者說相對路徑,程序直接全給你判斷瞭。
接下來會判斷pathToUse是否以“/”開頭,是的話prefix會加上“/”,現在的prefix有兩種情況,可能是”D:/”這種,也可能是”/”這種,而pathToUse肯定是“文件/API/JDK_API_1_6_zh_CN.CHM ”這種瞭。
String[] pathArray = delimitedListToStringArray( pathToUse, FOLDER_SEPARATOR );
看到這句代碼,我估計是把pathToUse給拆成字符串數組裡,就像是這樣,文件 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); }
先看看傳入的參數:
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倒著循環,每個字符都摘出來,然後當成字符串用,傳入deleteAny中,然後又是循環,對每個字符而言,如果charsToDelete中沒有這個字符,就在StringBuilder中添加這個字符。返回值是String。當然瞭,這個還沒用到。我們用到的是那個很復雜的else
我們遇到瞭一個循環,對pathToUse而言,從索引0開始,如果pathToUse中有”/”,就像文件/API/JDK_API_1_6_zh_CN.CHM 我們會得到“文件,然後還會進入deleteAny這個方法,參數inString就是”文件”,charsToDelete是null,突然發現charsToDelete的值為Null的話會直接返回InString,也就是“文件”。
返回到delimitedListToStringArray方法之後,接著往後循環,最終的結果就是實現瞭把pathToUse給切割成若幹個String的形式。
10:回到StringUtils的cleanPath方法
我們遇到瞭一個倒著的循環,如果說我們這個是特別正常的路徑,就相當於復制瞭,如果是以.或者..結尾的這些內容,我們就把它給忽略瞭。
我得承認上面這些過程真的好復雜,其實就是做瞭一件事,對輸入的路徑進行瞭處理,隻不過考慮的情況多瞭一點。所以我決定還是用debug來走一遍看看。
這時我用的是絕對路徑
終於熬到瞭這個方法的結束。
11:回到ClassPathResource的構造函數
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實例就有瞭path 和 classLoader這兩個關鍵屬性。
如果我們想獲取輸入流
@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這個類的意思,現在就能用一點瞭,先看看定義的一些屬性
private final String path ; private ClassLoader classLoader; private Class<?> clazz;
ClassPathResource有好幾個構造函數,有的構造函數會傳入classLoader,有的會傳入clazz,這個clazz就是相應的類在JVM上的實例,顯然上面的例子中並沒有這個東西,而classLoader是有的,所以通過classLoader獲取輸入流。
我覺得對ClassPathResource理解的更透徹瞭,雖然大部分時間都是在對path進行處理。
近期還要看看ClassLoader,還不是很清楚它的工作機制。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- Java實用工具之StringJoiner詳解
- Java中實現String字符串分割的3種方法
- 一篇文章看懂Java字符串操作
- java如何從不規則的字符串中截取出日期
- Java實現截取字符串的操作詳解