Tomcat類載入機制探析
title: Tomcat類載入機制探析
comments: false
toc: true
categories:
- Web伺服器
tags: - Tomcat
- 類載入
- 雙親委派
date: 2020-12-23 23:03:58
Java類載入機制,雙親委託模型相必大家已經熟的不能再熟了。本文就從Tomcat的原始碼級別上來探究一下類載入機制的祕密。
首先我們們還是老調重彈,看一下網上已經氾濫的一張Tomcat類載入關係圖
屬於JavaSE的BootstrapClassLoader、ExtClassLoader、AppClassLoader(這裡主要載入Tomcat啟動的Bootstrap類)在本文不再贅述;
-
CommonClassLoader載入common.loader屬性下的jar;一般是CATALINA_HOME/lib目錄下
-
CatalinaClassLoader載入server.loader屬性下的jar;預設未配置路徑,返回其父載入器即CommonClassLoader
-
SharedClassloader載入share.loader屬性下的jar;預設未配置路徑,返回其父載入器即CommonClassLoader
由於WebAppClassLoader需要等Tomcat的各個元件初始化完成之後才載入對應的server.xml配置檔案,解析對應Host下的docBase目錄尋找WEB-INF下的類檔案,並且會將該根目錄下的每個直接子目錄當作一個web專案載入,為了確保各個專案之間的相互獨立,每個專案都是單獨的WebAppClassLoader載入的,我們們後文再講。
CommonClassLoader、CatalinaClassLoader、SharedClassloader
先從原始碼角度來看看CommonClassLoader、CatalinaClassLoader、SharedClassloader這三者是如何繫結關係的?
org.apache.catalina.startup.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();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
呼叫了createClassLoader方法來建立物件,第二個引數是指定它的父級類載入器。可以看到catalinaLoader、sharedLoader均指明commonLoader為它的父級類載入器,這說明catalinaLoader、sharedLoader是同級類載入器,印證了上圖。
再看看createClassLoader方法怎麼做的:
// 建立基礎型別類載入器
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
// 根據“name+.loader”從系統配置中讀取需要載入的jar路徑
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
value = replace(value);
List<Repository> repositories = new ArrayList<>();
String[] repositoryPaths = getPaths(value);
for (String repository : repositoryPaths) {
// Check for a JAR URL repository
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
// 封裝路徑,指定其jar的型別:jar包、目錄等
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
repositories.add(new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(new Repository(repository, RepositoryType.DIR));
}
}
// 通過類載入工廠建立,repositories的值為當前Tomcat所在目錄下配置的jar子目錄,比如 Tomcat_Home/lib/
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
ClassLoaderFactory.createClassLoader是一個典型的工廠模式,遮蔽了類載入器物件初始化的細節:
public static ClassLoader createClassLoader(List<Repository> repositories,
final ClassLoader parent)
throws Exception {
if (log.isDebugEnabled())
log.debug("Creating new class loader");
// Construct the "class path" for this class loader
Set<URL> set = new LinkedHashSet<>();
if (repositories != null) {
// 根據jar包型別解析其Url,並新增到Set<URL> set
for (Repository repository : repositories) {
if (repository.getType() == RepositoryType.URL) {
URL url = buildClassLoaderUrl(repository.getLocation());
set.add(url);
} else if (repository.getType() == RepositoryType.DIR) {
File directory = new File(repository.getLocation());
directory = directory.getCanonicalFile();
if (!validateFile(directory, RepositoryType.DIR)) {
continue;
}
URL url = buildClassLoaderUrl(directory);
set.add(url);
} else if (repository.getType() == RepositoryType.JAR) {
File file=new File(repository.getLocation());
file = file.getCanonicalFile();
if (!validateFile(file, RepositoryType.JAR)) {
continue;
}
URL url = buildClassLoaderUrl(file);
set.add(url);
} else if (repository.getType() == RepositoryType.GLOB) {
File directory=new File(repository.getLocation());
directory = directory.getCanonicalFile();
if (!validateFile(directory, RepositoryType.GLOB)) {
continue;
}
String filenames[] = directory.list();
if (filenames == null) {
continue;
}
for (int j = 0; j < filenames.length; j++) {
String filename = filenames[j].toLowerCase(Locale.ENGLISH);
if (!filename.endsWith(".jar"))
continue;
File file = new File(directory, filenames[j]);
file = file.getCanonicalFile();
if (!validateFile(file, RepositoryType.JAR)) {
continue;
}
if (log.isDebugEnabled())
log.debug(" Including glob jar file "
+ file.getAbsolutePath());
URL url = buildClassLoaderUrl(file);
set.add(url);
}
}
}
}
// Construct the class loader itself
final URL[] array = set.toArray(new URL[set.size()]);
// 直接通過URLClassLoader建立ClassLoader
return AccessController.doPrivileged(
new PrivilegedAction<URLClassLoader>() {
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array);
else
return new URLClassLoader(array, parent);
}
});
}
可以看到通過ClassLoaderFactory.createClassLoader(List repositories, final ClassLoader parent)此方法建立的ClassLoader其本身是一個URLClassLoader,通過人為手工的方式分別指定了其父級類載入。
上文提到catalinaLoader會載入Tomcat本身的類,其主要目的是進行類載入隔離,與SharedClassloader區分開,這樣我們們開發人員編寫的web專案就不能直接訪問到Tomcat的類,造成安全問題了。 那麼又是怎麼實現的呢?我們們接著看Tomcat的啟動前的初始方法org.apache.catalina.startup.Bootstrap#init()
public void init() throws Exception {
// 初始化commonLoader、catalinaLoader、sharedLoader3個類載入器
initClassLoaders();
// 直接設定當前執行緒的類載入器為catalinaLoader
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
// 使用當前執行緒的catalinaLoader來載入Catalina類
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
// 以下整段表示:Catalina初始化後得到的物件startupInstance呼叫setParentClassLoader方法,將sharedLoader設定為父載入器
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
從原始碼中可以看到步驟:
- 用catalinaLoader載入了"org.apache.catalina.startup.Catalina"類;
- 立即通過setParentClassLoader方法指定其父載入器為sharedLoader,這樣Catalina物件也可訪問sharedLoader下的類了;
如何為每一個webApp專案定義一個獨立的WebAppClassLoader?
我們們從原始碼角度來分析一下。
Tomcat的各容器呼叫流程不是本文關注的重點,直接檢視webApp的類載入器初始化的程式碼位置org.apache.catalina.loader.WebappLoader#startInternal()
// 每一個專案context均會執行此方法
protected void startInternal() throws LifecycleException {
// 省略部分程式碼
// Construct a class loader based on our current repositories list
try {
// 建立WebApp的類載入器
classLoader = createClassLoader();
// 獲取一個webapp專案的類載入路徑:WEB-INF/classes、WEB-INF/lib/
classLoader.setResources(context.getResources());
// delegate是否遵循類載入的雙親委派模型,預設為false
classLoader.setDelegate(this.delegate);
// 執行類載入
((Lifecycle) classLoader).start();
// 省略
} catch (Throwable t) {
// 省略
}
// 省略
}
private String loaderClass = ParallelWebappClassLoader.class.getName();
/**
* Create associated classLoader.
*/
private WebappClassLoaderBase createClassLoader()
throws Exception {
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
if (parentClassLoader == null) {
parentClassLoader = context.getParentClassLoader();
}
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}
當每一個HOST執行org.apache.catalina.startup.HostConfig#deployApps()方法釋出所有的專案時,其工作目錄下的每個webapp專案均會執行org.apache.catalina.loader.WebappLoader#startInternal()方法,並在其中使用createClassLoader()建立類載入器。可以從createClassLoader()方法看到 WebappClassLoaderBase的真正實現類為ParallelWebappClassLoader。
ParallelWebappClassLoader類繼承於WebappClassLoaderBase,類結構如下:
WebappClassLoaderBase繼承於URLClassLoader,由URLClassLoader可指定載入位置的Path來進行類載入。
還記得上面的 this.delegate 欄位嗎?預設為false,表示不使用雙親委派機制,我們再看看是如何破壞雙親委派機制的。方法位置:org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean)
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
// 省略一些檢查程式碼
// (0) 從本地位元組碼快取中查詢
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
// (0.1) 從已經載入過的類中查詢
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// (0.2) 嘗試使用系統類載入器載入該類,以防止webapp覆蓋Java SE類。This implements SRV.10.7.2
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
//使用getResource,因為如果Java SE類載入器無法提供該資源,它將不會觸發昂貴的ClassNotFoundException。
//但是,在極少數情況下在安全管理器下執行時(有關詳細資訊,請參見https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 //),此呼叫可能觸發ClassCircularityError。
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
// 系統類載入器不適合載入此類,捕捉異常不再往外丟擲
ExceptionUtils.handleThrowable(t);
tryLoadingFromJavaseLoader = true;
}
// 嘗試通過JavaSE類載入器載入
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.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);
}
}
}
//預設為不使用雙親委派,呼叫filter進行過濾是否可以委派載入此方法,如果是JSP,EL表示式,Tomcat等一些類則可以委派
boolean delegateLoad = delegate || filter(name, true);
// (1) 如果需要,委託給我們的父載入器
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (2) 從本地搜尋查詢類即自我載入
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) 實在找不到,無條件委託給父載入器
if (!delegateLoad) {
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
// 所有方法均載入失敗,返回ClassNotFoundException
throw new ClassNotFoundException(name);
}
從原始碼中,可以提取為以下步驟:
- (0) 從本地位元組碼快取中查詢
- (0.1) 從已經載入過的類中查詢
- (0.2) 嘗試使用系統類載入器載入該類,以防止webapp覆蓋Java SE類。
- (0.3) 系統類載入器不適合載入此類
- (0.4) 嘗試通過JavaSE類載入器載入
- (0.5) 使用SecurityManager時訪問此類的許可權
- (1) 判斷是否雙親委派(一般為false),如果需要,委託給我們的父載入器
- (2) 使用自己載入器進行類載入,注意此步驟如果執行則破壞了雙親委派原則,因為沒有讓父類載入器進行載入。
- (3) 實在找不到,無條件委託給父載入器
- 所有方法均載入失敗,返回ClassNotFoundException
總結
Java的雙親委派機制無疑是一個非常優秀的設計,它有以下優點:
- 採用雙親委派模式的是好處是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係,通過這種層級關可以避免類的重複載入,當父親已經載入了該類時,就沒有必要子ClassLoader再載入一次。
- 增加系統安全,java核心API中定義型別不會被隨意替換。
但是在Tomcat這種需要多專案部署時,其反而可能會有一些弊端,比如剛好部署了A,B兩個位元組碼完全相同的系統,如果是傳統的雙親委派機制,則後載入的系統的類不會載入成功,如果有類靜態變數則會被2個系統共享,引起系統異常。
相關文章
- 圖解tomcat類載入機制(tomcat7)圖解Tomcat
- Tomcat 第六篇:類載入機制Tomcat
- 類載入機制
- 虛擬機器類載入機制:類載入時機虛擬機
- 類的載入機制
- JVM:類載入機制JVM
- JVM類載入機制JVM
- java類載入機制Java
- 探祕類載入器和類載入機制
- 類載入機制總結
- 類載入機制與反射反射
- JVM-類載入機制JVM
- 虛擬機器類載入機制_類載入的過程虛擬機
- 類載入流程,類載入機制及自定義類載入器
- 自定義類載入器驗證類載入機制
- 虛擬機器類載入機制虛擬機
- Java類載入機制總結Java
- JVM初探(三):類載入機制JVM
- JVM類載入機制小結JVM
- 談談 Java 類載入機制Java
- Flink 類載入機制介紹
- 虛擬機器類載入機制_類載入時機和類的生命週期虛擬機
- Java基礎-類載入器以及載入機制Java
- 【JVM進階之路】十四:類載入器和類載入機制JVM
- java虛擬機器類載入機制Java虛擬機
- Java 虛擬機器類載入機制Java虛擬機
- JVM學習(三)——類載入機制JVM
- Java類載入機制-雙親委派Java
- 理解JVM(四):JVM類載入機制JVM
- 玩命學JVM(二)—類載入機制JVM
- JVM之類載入機制總結JVM
- 深入理解OSGi類載入機制
- 徹底剖析JVM類載入機制JVM
- Java 技術之類載入機制Java
- Tomcat是如何載入類的Tomcat
- Java虛擬機器(六):類載入機制Java虛擬機
- JVM(7)-虛擬機器類載入機制JVM虛擬機
- 虛擬機器系列 | JVM類載入機制虛擬機JVM