問題描述
在開發一個Spring Boot應用的過程中發現了這樣一個問題:在IDE(Intellij)中程式可以除錯成功,可以執行,但是當用Maven打包(用的spring-boot-maven-plugin)過後,在命令列中執行這個jar包就會報錯。錯誤指向的是一處ClassLoader的呼叫:
URL url = ClassLoader.getSystemResource("data/test.xml");
複製程式碼
也就是說,在IDE中執行通過ClassLoader.getSystemResource()
能找到data/test.xml
,但是打包過後找不到,會返回null。
那麼問題就是出在這個ClassLoader的呼叫上面了。
解決辦法
我的應用場景是將部分資源放在classpath目錄下,然後通過程式碼來載入,有很多其他的開源工具也需要載入一些靜態檔案(或配置檔案),比如log4j,因此可以參考log4j的解決方案。實際上,避免直接使用ClassLoader,換成是使用log4j中的Loader來載入資源就完全可以解決這個問題。
log4j中載入配置檔案是通過一個自定義的Loader類來實現的,Loader中封裝了ClassLoader的getResource方法和getClass方法。getResource方法的實現邏輯是:
- 如果jdk版本是1.2及以上,則(通過反射)呼叫當前執行緒物件的
getContextClassLoader()
方法來獲取ClassLoader,然後載入資源 - 如果jdk版本低於1.2,則使用載入Loader類的ClassLoader來載入資源:
Loader.class.getClassLoader()
Loader.java中getResource方法的定義為:
static public URL getResource(String resource) {
ClassLoader classLoader = null;
URL url = null;
try {
if(!java1 && !ignoreTCL) {
classLoader = getTCL();
if(classLoader != null) {
LogLog.debug("Trying to find ["+resource+"] using context classloader "
+classLoader+".");
url = classLoader.getResource(resource);
if(url != null) {
return url;
}
}
}
// We could not find resource. Ler us now try with the
// classloader that loaded this class.
classLoader = Loader.class.getClassLoader();
if(classLoader != null) {
LogLog.debug("Trying to find ["+resource+"] using "+classLoader
+" class loader.");
url = classLoader.getResource(resource);
if(url != null) {
return url;
}
}
} catch(IllegalAccessException t) {
LogLog.warn(TSTR, t);
} catch(InvocationTargetException t) {
if (t.getTargetException() instanceof InterruptedException
|| t.getTargetException() instanceof InterruptedIOException) {
Thread.currentThread().interrupt();
}
LogLog.warn(TSTR, t);
} catch(Throwable t) {
//
// can't be InterruptedException or InterruptedIOException
// since not declared, must be error or RuntimeError.
LogLog.warn(TSTR, t);
}
// Last ditch attempt: get the resource from the class path. It
// may be the case that clazz was loaded by the Extentsion class
// loader which the parent of the system class loader. Hence the
// code below.
LogLog.debug("Trying to find ["+resource+
"] using ClassLoader.getSystemResource().");
return ClassLoader.getSystemResource(resource);
}
複製程式碼
getTCL方法的定義為:
private static ClassLoader getTCL() throws IllegalAccessException,
InvocationTargetException {
// Are we running on a JDK 1.2 or later system?
Method method = null;
try {
method = Thread.class.getMethod("getContextClassLoader", null);
} catch (NoSuchMethodException e) {
// We are running on JDK 1.1
return null;
}
return (ClassLoader) method.invoke(Thread.currentThread(), null);
}
複製程式碼
問題出現原因
真正的解決辦法比上面簡單得多。我在原來的程式碼裡面呼叫的是getSystemResource方法,這個方法是通過ClassLoader找到SystemClassLoader,然後通過這個SystemClassLoader來載入資源。實際上應該呼叫getResource方法,指定載入當前類(或任意一個自定義類)的ClassLoader來呼叫即可:
URL url = Loader.class.getClassLoader.getResource("data/test.xml");
複製程式碼
無論ClassLoader是通過靜態方法getSystemResource,還是通過一個ClassLoader例項(Loader.class.getClassLoader())呼叫getSystemResource,最後都需要獲取到SytemClassLoader來載入資源。
在程式中新增下面的程式碼:
logger.info("ClassLoader.getSystemClassLoader().getClass()");
logger.info(ClassLoader.getSystemClassLoader().getClass().getName());
複製程式碼
在打包前後輸出的都是:
ClassLoader.getSystemClassLoader().getClass()
sun.misc.Launcher$AppClassLoader
複製程式碼
在打包以前可以用sun.misc.Launcher$AppClassLoader
來載入資源,因為程式是直接在target/classes
裡面查詢,而打包之後jar包的結構發生了變化,載入jar包內部的資源需要用org.springframework.boot.loader.LaunchedURLClassLoader
。
把上面的程式碼改為:
logger.info("Loader.class.getClassLoader()");
logger.info(Loader.class.getClassLoader().getClass().getName());
複製程式碼
在打包前後輸出的結果中就能看到他們的載入器不同:
Loader.class.getClassLoader()
sun.misc.Launcher$AppClassLoader
Loader.class.getClassLoader()
org.springframework.boot.loader.LaunchedURLClassLoader
複製程式碼
總結
ClassLoader#getResource
和ClassLoader#getSystemResource
不同,前者是呼叫載入器例項來載入資源,而後者是呼叫系統載入器載入資源;- Spring Boot專案打包之後應該用Spring提供的ClassLoader來載入資源(通過自定義類的getClassLoader即可),否則可能會出錯;
- 直接參考成型的開原始碼能快速解決問題