Tomcat 內部定義了多個 ClassLoader,以便應用和容器訪問不一樣存儲庫中的類和資源,同時達到應用間類隔離的目的。本文首發於公衆號:頓悟源碼。java
類加載就是把編譯生成的 class 文件,加載到 JVM 內存中(永久代/元空間)。git
類加載器之因此能實現類隔離,是由於兩個類相等的前提是它們由同一個類加載器加載,不然一定不相等。github
JVM 在加載時,採用的是一種雙親委託機制,當類加載器要加載一個類時,加載順序是:緩存
這個機制的好處就是可以保證核心類庫不被覆蓋。tomcat
而按照 Servlet 規範的建議,Webapp 加載器略有不一樣,它首先會在本身的資源庫中搜索,而不是向上委託,打破了標準的委託機制,來看下 Tomcat 的設計和實現。微信
Tomcat 總體類加載器結構以下:app
其中 JDK 內部提供的類加載器分別是:源碼分析
Tomcat 自定義實現的類加載器分別是:優化
在實現時,上圖不是繼承關係,而是經過組合體現父子關係。Tomcat 類加載器的源碼類圖:this
Common、Catalina 、Shared 它們都是 StandardClassLoader 的實例,在默認狀況下,它們引用的是同一個對象。其中 StandardClassLoader 與 URLClassLoader 沒有區別;WebappClassLoader 則按規範實現如下順序的查找並加載:
接下來看下源碼實現。
common 類加載器是在 Bootstrap 的 initClassLoaders 初始化的,源碼以下:
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
// 指定倉庫路徑配置文件前綴和父加載器,建立 ClassLoader 實例
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
複製代碼
能夠看到分別建立了三個類加載器,createClassLoader 就是根據配置獲取資源倉庫地址,最後返回一個 StandardClassLoader 實例,核心代碼以下:
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent; // 若是沒有配置,則返回傳入的父加載器
ArrayList repositoryLocations = new ArrayList();
ArrayList repositoryTypes = new ArrayList();
...
// 獲取資源倉庫路徑
String[] locations = (String[]) repositoryLocations.toArray(new String[0]);
Integer[] types = (Integer[]) repositoryTypes.toArray(new Integer[0]);
// 建立一個 StandardClassLoader 對象
ClassLoader classLoader = ClassLoaderFactory.createClassLoader
(locations, types, parent);
...
return classLoader;
}
複製代碼
類加載器初始化完畢後,會建立一個 Catalina 對象,最終會調用它的 load 方法,解析 server.xml 初始化容器內部組件。那麼容器,好比 Engine,又是怎麼關聯到這個設置的父加載器的呢?
Catalina 對象有一個 parentClassLoader 成員變量,它是全部組件的父加載器,默認是 AppClassLoader,在此對象建立完畢時,會反射調用它的 setParentClassLoader 方法,將父加載器設爲 sharedLoader。
而 Tomcat 內部頂級容器 Engine 在初始化時,Digester 有一個 SetParentClassLoaderRule 規則,會將 Catalina 的 parentClassLoader 經過 Engine.setParentClassLoader 方法關聯起來。
答案是使用 Thread.getContextClassLoader() - 當前線程的上下文加載器,該加載器可經過 Thread.setContextClassLoader() 在代碼運行時動態設置。
默認狀況下,Thread 上下文加載器繼承自父線程,也就是說全部線程默認上下文加載器都與第一個啓動的線程相同,也就是 main 線程,它的上下文加載器是 AppClassLoader。
Tomcat 就是在 StandardContext 啓動時首先初始化一個 WebappClassLoader 而後設置爲當前線程的上下文加載器,最後將其封裝爲 Loader 對象,藉助容器之間的父子關係,在加載 Servlet 類時使用。
Web 應用的類加載是由 WebappClassLoader 的方法 loadClass(String, boolean) 完成,核心代碼以下:
public synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
...
Class clazz = null;
// (0) 檢查自身內部緩存中是否已經加載
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve) resolveClass(clazz);
return (clazz);
}
// (0.1) 檢查 JVM 的緩存中是否已經加載
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve) resolveClass(clazz);
return (clazz);
}
// (0.2) 嘗試使用系統類加載加載,防止覆蓋 J2SE 類
try {
clazz = system.loadClass(name);
if (clazz != null) {
if (resolve) resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {// Ignore}
// (0.5) 使用 SecurityManager 檢查是否有此類的訪問權限
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name);
// (1) 是否委託給父類,這裏默認爲 false
if (delegateLoad) {
...
}
// (2) 嘗試查找本身的存儲庫並加載
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve) resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {}
// (3) 若是此時還加載失敗,那麼將加載請求委託給父加載器
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = loader.loadClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve) resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {}
}
// 最後加載失敗,拋出異常
throw new ClassNotFoundException(name);
}
複製代碼
在防止覆蓋 J2SE 類的時候,版本 Tomcat 6,使用的是 AppClassLoader,rt.jar 核心類庫是由 Bootstrap Classloader 加載的,可是在 Java 代碼是獲取不了這個加載器的,在高版本作了如下優化:
ClassLoader j = String.class.getClassLoader();
if (j == null) {
j = getSystemClassLoader();
while (j.getParent() != null) {
j = j.getParent();
}
}
this.javaseClassLoader = j;
複製代碼
也就是使用盡量接近 Bootstrap 加載器的類加載器。
相信大部分人都遇到過 ClassNotFoundException 這個異常,這背後就涉及到了類加載器,對加載的原理有必定的瞭解,有助於排查問題。
爲了更好的理解,這裏模擬實現了此類加載器。
加載器源碼:Loader.java
搜索微信公衆號「頓悟源碼」,獲取更多源碼分析和造的輪子。