起因
針對下面這個代碼
public static void loadFile(){
try(InputStream is = XXX.class.getClassLoader.getResourceAsStream("xxxxx.txt"){
//xxxxxx
}
}
有同僚提出為啥此處要用“XXX” claas,用其它的class是否可行?
然後我一通解釋,甚至還般出了
Thread.currentThread().getContextClassLoader().loadClass("xxx.xxx.xx")
的方案。但是進一步探讨時,發現成功的給自己挖了個坑。有以下幾個問題自己都解釋不通。
- Thread上下文擷取到的類加載器與Class.getClassLoader()拿到的有什麼差別
- 在加載一個類的時候,是如何找到一個類的位置的
- 在加載檔案的檔案,第三方jar包中的配置檔案與自己項目下的Resource目錄下的加載方式有沒有什麼差別,是否需要不同的類的ClassLoader
- 在加載檔案或者類資源的時候,對檔案的命名有何要求
在上面幾個問題的引導下,重新研究了下ClassLoader的相關文檔,并且寫例子驗證了這些問題,進而有了此文。
ClassLoader原理闡述
ClassLoader,顧名思義,就是類加載器。當我們調用一個new語句時,相應的class檔案需在被加載到Jvm中。此時ClassLoader就需要負責查找class,讀class檔案,校驗,連接配接,最終加載到Jvm中
- java提供的預設ClassLoader
- BootStrap ClassLoader: 加載位于
處的class檔案,是以可以通過System.getProperty("sun.boot.class.path")
參數設定BootStrap ClassLoader負責加載的類sun.boot.class.path
- ExtClassLoader:加載位于
的類System.getProperty("java.ext.dirs")
- AppClassLoader:也稱
, 加載位于SystemClassLoader
處的類System.getProperty("java.class.path")
- BootStrap ClassLoader: 加載位于
-
ClassLoader的雙親委托機制
ClassLoader是一個虛類,除BootStrap ClassLoader外,每一個ClassLoader都需要繼承ClassLoader,并且每個都有一個其父ClassLoader的引用(同樣,BootStrap除外)。BootStrap ClassLoader是C++ 語言寫的,在系統啟動的時候,就會啟動BootStrap類加載器。BootStrap ClassLoader是ExtClassLoader的父加載器,但是如果擷取ExtClassLoader的父加載器,拿到是null;ExtClassLoader 是AppClassLoader的父加器
但是雙親委托機制并不是絕對的,像後文中提到的SpringBoot的 RestartClassLoader就打破了該機制。
在jvm啟動的時候,會加載類
,在該類的造函數中,會建立ExtClassLoader及AppClassLoader,并且把ExtClassLoader設定為AppClassLoader的父ClassLoader,同時設定線程的上下文加載器為AppClassLoader。sun.misc.Launcher
- 類加載過程(預設的
流程)loadClass
- 首先AppClassLoader會檢查是否加載過該類,如果加載過,直接傳回
- 沒有加載過,委托父加載器進行加載(父加載器執行同樣的流程)
- 父加載器加載成功,直接傳回,如果沒有加載成功,則目前加載器開始查找Class再進行加載。
流程,隻實作其loadClass
findClass
方法即可,這樣就可以不破壞加載器的雙親委機制。是以對于自定義的ClassLoader而言,實作如何查找類,在哪裡查找類就是關鍵。
對于查找資源檔案,與加載類一緻,在下面的源碼分析中會提到。
對于線程的上下文加載器,等下在源碼分析中也可以看到一點,總體而言就是将一下加載器儲存線上程的上下文中,在某些特定的場景中使用,比如tomcat中會使用到。同時線程的上下文加載器與目前的類可能并不一是緻。
ClassLoader相關源碼分析
看 com.sun.Launcher
源碼(分析預設的幾個類加載器的構造過程)
com.sun.Launcher
public class Launcher {
//系統的ClassLoader,後面會被指派為AppClassLoader
private ClassLoader loader;
//BootStrap ClassLoader要加載的類的範圍
private static String bootClassPath = System.getProperty("sun.boot.class.path");
public Launcher() {
// 建立ExtClassLoader
ClassLoader extcl = ExtClassLoader.getExtClassLoader();
// 建立AppClassLoader,同時設定extcl為其父加載器
loader = AppClassLoader.getAppClassLoader(extcl);
//設定線程上下文類加載器為 AppClassLoader Thread.currentThread().setContextClassLoader(loader);
}
/*
* 擷取類加載器
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* 具體加載ExtClassLoader的過程,并且可以看出,其繼承自URLClassLoader
*/
static class ExtClassLoader extends URLClassLoader {
private File[] dirs;
// 建立方法
public static ExtClassLoader getExtClassLoader() throws IOException
{
final File[] dirs = getExtDirs();
try {
return (ExtClassLoader) AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public Object run() throws IOException {
int len = dirs.length;
for (int i = ; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
/*
* Creates a new ExtClassLoader for the specified directories.
*/
public ExtClassLoader(File[] dirs) throws IOException {
// 第二個參數就是parent ClassLoader,此處可以看到為null
super(getExtURLs(dirs), null, factory);
this.dirs = dirs;
}
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
// 處理s相關的内容
return dirs;
}
}
/**
* 建立AppClassLoader
*/
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
// 參數 System.getProperty("java.class.path")
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[] : getClassPath(s);
return (AppClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
URL[] urls =
(s == null) ? new URL[] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
/*
* Creates a new AppClassLoader
*/
AppClassLoader(URL[] urls, ClassLoader parent) {
// 此處可以看到ExtClassLoader 是 AppClassLoader的父ClassLoader
super(urls, parent, factory);
}
}
}
上面代碼可以看到,在建立Launcher的時候,建立了ExtClassLoader,并且将其parent ClassLoader設定為null,建立了AppClassLoader将ExtClassLoader設定為其parent ClassLoader。 同時可以看到每個ClassLoader都繼承自URLClassLoader,并且可以看到各ClassLoader負責加載的類的範圍
分析ClassLoader的源碼
public abstract class ClassLoader {
// 儲存的地父加載器的引用
private final ClassLoader parent;
// 加載class檔案的核心代碼
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果父加載器不為空,則委托父加載器進行加載,可以看到雙親委托機制
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 加載器為空,即ExtClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//沒有加載到類,抛異常後,吞掉
}
// 如果父加載器沒有加載到,輪到自己進行加載
if (c == null) {
// 這個方法是一個虛方法,還未實作,子類實作即可
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
從上面代碼可以看到,查找類的時候,采用雙親委托機制,先委托父類進行查找類,如果找不到自己再進行查找。
再來簡單介紹一個URLClassLoader:ClassLoader中有一個URLClassPath屬性,URLClassPath包含一個List的屬性,該屬性就儲存着目前ClassLoader可以查找的範圍。當URL以’/’結尾時,說明是一個directory,當是以非’/’結尾的時候,說明是一個jar包。當查找類或者資源檔案時,周遊該URL集合,根據是directory還是jar包分别進行處理。
再看查找資源檔案,getResource的方法:
//class 的getResource檔案, Class 類
public java.net.URL getResource(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResource(name);
}
return cl.getResource(name);
}
可以看到,Class.getResource() 最後是通過調用
ClassLoader.getResource()
實作的。目前在此之前,先進行了
resolveName(name)
方法。該方法的作用是擷取該資源檔案相關對于ClassPath的路徑,即如果以’/’打頭,則認為是一個完整路徑,去掉’/’,如果以非’/’打頭,則認為是相對目前類檔案的地方,則會将目前Class檔案的package名加到當明的name前,并且将’.’用’/’替換,形成相對于ClassPath的路徑。
ClassLoader的的getResouce的方法
// 同樣執行雙親委托機制
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
// findResource 是一個虛方法,子類可以實作,與類加載的過程類似。
// 而對于AppClassLoader以及ExtClassLoader,即從adsw包含的Url屬性中查找相關的資源,包含從jar檔案中及目錄中。
url = findResource(name);
}
return url;
}
實驗證明
- 自定義類加載器,加載非classPath下的檔案,并且驗證不同的類加載器加載到的類是否是同一個類
執行結果//先定義一個我們想要加載的類 package com.person; public class Test{ // 該方法輸出自己的類加載器 public void sayClassLoader(){ System.out.println("classLoader:" + Test.class.getClassLoader()); } } public class MainTest { public static void main(String[] args) throws Exception { // 我們把剛定義的java檔案編譯,放在/tmp/com/person下,其中 ‘/com/person’是對應java檔案的package String path = "/tmp/"; URL url = new File(path).toURI().toURL(); List<Class<?>> classes = Lists.newArrayList(); for (int i = ; i < ; i++) { Thread thread = new Thread(() -> { try { //使用自定義的ClassLoader MyClassLoader loader = new MyClassLoader(new URL[]{url}, MainTest.class.getClassLoader()); Class<?> aClass = loader.loadClass("com.person.Test"); // 反射構執行個體以及調用 sayClassLoader 方法 Object instance = aClass.newInstance(); Method method = aClass.getMethod("sayClassLoader"); method.invoke(instance); classes.add(aClass); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) { e.printStackTrace(); } }); thread.start(); } // 等待兩個線程執行完畢 Thread.sleep(); //判定兩個類是否是同一個類 System.out.println(classes.get() == classes.get()); } } //自定義的類加載器 class MyClassLoader extends URLClassLoader{ public MyClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } }
上面案例中,自定義的類加載器繼承了URLClassLoader,通過上面的case可以看出,自定義的類加載器加載了自定義的目錄上的檔案,并且發現兩個ClassLoader加載的類,即使是同一個類檔案,但是也不是同一個類。classLoader:com.yqg.collection.MyClassLoader@20c489d2 classLoader:com.yqg.collection.MyClassLoader@5e9b8a25 false
- 驗證類的雙新委托機制
運作結果如下public static void main(String[] args) throws Exception { URLClassLoader classLoader = (URLClassLoader) MainTest.class.getClassLoader(); // 這個反射的方法用于增加該ClassLoader的查找範圍,現在增加了/tmp目錄 Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); method.setAccessible(true); String path = "/tmp/"; URL url = new File(path).toURI().toURL(); method.invoke(classLoader, url); //用自定義的類加載器進行加載 MyClassLoader loader = new MyClassLoader(new URL[]{url}, MainTest.class.getClassLoader()); Class<?> aClass = loader.loadClass("com.person.Test"); // 輸出最終類的加載器 System.out.println(aClass.getClassLoader()); }
從上面看出,sun.misc.Launcher$AppClassLoader@18b4aac2
類雖然是通過調用MyClassLoader進行加載但是根據雙親委托機制,最終由其父加載器AppClassLoader完成加載。com.person.Test
結論
- 類通過ClassLoader進行加載,ClassLoader持有其父加載器的引有,正常情況下,當需要加載一個類或者一個資源檔案時,委托父加載器進行加載,當父加載器加載不到時,由目前加載器進行加載。但是這并不是必須的,像ogsi,tomcat就都有違反此機制的情況。
- 加載資源檔案時,與加載類檔案一緻,隻要加載器是一樣的,其可加載的範圍就是一樣的,配置檔案可以是目前工程的配置檔案,也可能是第三方jar的配置檔案。
- 當有多個資源檔案時,與類加載器的搜尋順序相關。是以可以在findResouce的時候,設定優先級及順序。
- 不同的類加載器加載到的類檔案,不是同一個類,這個可用于在一些特定的場景中,比如tomcat的WebappClassLoader用于隔離多個Context,SpringBoot的RestartClassLoader僅用于加載适用于熱更新的類,像第三方jar包則由其它的類加載器進行加載。
- 查找類檔案與查找檢查資源檔案基本上是一緻的,也取決于類加載器的實作。
其它case分析(SpringBoot RestartClassLoader)
// 同樣繼承了URLClassLoader
// 該類加載器隻關心可熱更新的類(自己項目),而第三方jar包會有其它的ClassLoader進行加載,當項目中的類發生變更時,隻需替換目前項目中的類即可,不需要加載全部的類
public class RestartClassLoader extends URLClassLoader implements SmartClassLoader {
/**
* Create a new {@link RestartClassLoader} instance.
* @param parent the parent classloader
* @param updatedFiles any files that have been updated since the JARs referenced in
* URLs were created.
* @param urls the urls managed by the classloader
* @param logger the logger used for messages
*/
public RestartClassLoader(ClassLoader parent, URL[] urls,
ClassLoaderFileRepository updatedFiles, Log logger) {
super(urls, parent);
Assert.notNull(parent, "Parent must not be null");
Assert.notNull(updatedFiles, "UpdatedFiles must not be null");
Assert.notNull(logger, "Logger must not be null");
this.updatedFiles = updatedFiles;
this.logger = logger;
if (logger.isDebugEnabled()) {
logger.debug("Created RestartClassLoader " + toString());
}
}
// 重寫loadClass方法
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
// 先在目前的updatedFiles中找
ClassLoaderFile file = this.updatedFiles.getFile(path);
if (file != null && file.getKind() == Kind.DELETED) {
throw new ClassNotFoundException(name);
}
Class<?> loadedClass = findLoadedClass(name);
// 如是沒有找到
if (loadedClass == null) {
try {
// 先自行進行加載
loadedClass = findClass(name);
}
catch (ClassNotFoundException ex) {
//如果沒有加載到,再調用父加載器進行加載,從這個地方可以看到,為了滿足熱更新,違背了雙親委托原則。
loadedClass = getParent().loadClass(name);
}
}
if (resolve) {
resolveClass(loadedClass);
}
return loadedClass;
}
}