前言:
本文是基於 ClassLoader雙親委派機制源碼分析 瞭解過正統JDK類加載機制及其實現原理的基礎上,進而分析這種思想如何應用到Tomcat這個web容器中,從源碼的角度對 違反ClassLoader雙親委派機制三部曲之首部——JDBC驅動加載 中提出的Tomcat是如何完成多個web應用之間相互隔離,又如何保證多個web應用都能加載到基礎類庫
的問題進行了解答,我們按如下的思路佈局整篇文章:
- 先給出Tomcat整體的類加載體系結構
- 通過查看源碼驗證該類加載體系的正確性
- 總結Tomcat如何設計保證多應用隔離
另外本文是基於Tomcat7的源碼進行分析的,因此讀者最好先搭建一套基於Tomcat7的環境,以便查閱源碼以及運行調試,可以按照該文章的方式進行搭建:Tomcat源碼導入Idea
Tomcat類加載體系結構
圖1. Tomcat整體類加載體系結構
Tomcat本身也是一個java項目,因此其也需要被JDK的類加載機制加載,也就必然存在引導類加載器、擴展類加載器和應用(系統)類加載器。Tomcat自身定義的類加載器主要由圖中下半部分組成,Common ClassLoader
作爲Catalina ClassLoader
和Shared ClassLoader
的parent,而Shared ClassLoader
又可能存在多個children類加載器WebApp ClassLoader
,一個WebApp ClassLoader
實際上就對應一個Web應用,那Web應用就有可能存在Jsp頁面,這些Jsp頁面最終會轉成class類被加載,因此也需要一個Jsp的類加載器,就是圖中的JasperLoder
需要注意的是,在代碼層面Catalina ClassLoader
、Shared ClassLoader
、Common ClassLoader
對應的實體類實際上都是URLClassLoader
或者SecureClassLoader
,一般我們只是根據加載內容的不同和加載父子順序的關係,在邏輯上劃分爲這三個類加載器;而WebApp ClassLoader
和JasperLoader
都是存在對應的類加載器類的
下面我們從源碼設計的角度驗證圖中類加載器的設計
源碼分析Tomcat類加載機制
Tomcat的啓動入口在Bootstrap.class
中
圖2. Tomcat啓動入口
其中初始化類加載器的流程在bootstrap.init();
中,如下“代碼清單1”
public void init()
throws Exception
{
// Set Catalina path
setCatalinaHome();
setCatalinaBase();
// (1) 初始化 classLoader
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
//加載 org.apache.catalina.startup.Catalina class
Class<?> startupClass =
catalinaLoader.loadClass
("org.apache.catalina.startup.Catalina");
// (2) 實例化 Catalina 實例
Object startupInstance = startupClass.newInstance();
// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
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;
}
(1)處註釋的代碼主要進行類加載的初始化以及形成類加載器的關係初始化,繼續跟進
圖3. initClassLoaders()方法
這裏紅線處的代碼實際上創建了三個ClassLoader對象,其名稱和Tomcat類加載關係圖中的類加載器高度一致,那麼我們猜測createClassLoader(String,ClassLoader)
方法可能就是創建Tomcat自定義類加載器的方法之一,繼續往下看 “ 代碼清單2 ”
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
// (1) 根據名稱查找特定的配置
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
value = replace(value);
List<Repository> repositories = new ArrayList<Repository>();
StringTokenizer tokenizer = new StringTokenizer(value, ",");
while (tokenizer.hasMoreElements()) {
String repository = tokenizer.nextToken().trim();
if (repository.length() == 0) {
continue;
}
// 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
}
// Local repository
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));
}
}
// (2) 類加載器工廠創建特定類加載器
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
代碼清單中(1)處註釋是根據上圖中傳遞的“名稱”加上後綴.loader
去某個配置文件加載文件,爲了突出重點,這裏直接給出結論,其加載的內容爲/org/apache/catalina/startup/catalina.properties
,比如要加載 common.loader
對應的value,其在文件中的值爲${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
,也就是說Common ClassLoader要加載的路徑是這些,是Tomcat運行要使用的公共組件,比如servlet-api.jar
、catalina.jar
等;而我們發現當要加載server.loader
和shared.loader
時,其key在配置文件中的value爲空,也就是說,默認情況下Catalina ClassLoader和Shared ClassLoader(Tomcat整體類加載體系結構圖中紅色虛線內)都不存在,只有Common ClassLoader
方法中的第二個參數表示創建類加載器的父類加載器是哪個,再看initClassLoaders()方法
圖中代碼,在創建catalinaLoader
和sharedLoader
時,父類加載器傳入的實際上就是commonLoader
,以此可以驗證圖1中Catalina ClassLoader
、Shared ClassLoader
和Common ClassLoader
的父子關係。而common ClassLoader
的父類加載器參數傳遞的爲null,爲什麼null就會導致該類加載器的父類加載器爲System ClassLoader
呢?我們需要進入代碼清單2中看註釋(2)處標識的代碼 代碼清單3
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<URL>();
// 加載指定路徑下的資源對象
if (repositories != null) {
for (Repository repository : repositories) {
if (repository.getType() == RepositoryType.URL) {
URL url = buildClassLoaderUrl(repository.getLocation());
if (log.isDebugEnabled())
log.debug(" Including URL " + url);
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);
if (log.isDebugEnabled())
log.debug(" Including directory " + url);
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);
if (log.isDebugEnabled())
log.debug(" Including jar file " + url);
set.add(url);
} else if (repository.getType() == RepositoryType.GLOB) {
File directory=new File(repository.getLocation());
directory = directory.getCanonicalFile();
if (!validateFile(directory, RepositoryType.GLOB)) {
continue;
}
if (log.isDebugEnabled())
log.debug(" Including directory glob "
+ directory.getAbsolutePath());
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()]);
if (log.isDebugEnabled())
for (int i = 0; i < array.length; i++) {
log.debug(" location " + i + " is " + array[i]);
}
// 返回創建的類加載器
return AccessController.doPrivileged(
new PrivilegedAction<URLClassLoader>() {
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array);
else
return new URLClassLoader(array, parent);
}
});
}
大塊的if
中的代碼實際上是對資源進行轉化加載的過程,而return
部分纔是返回類加載器的部分,代碼根據是否有parent調用了URLClassLoader
不同的構造器,Common ClassLoader
調用的是沒有parent的構造器
圖4. Common ClassLoader的parent創建的底層關鍵代碼
按紅線所畫Common ClassLoader
的parent實際上是JDK中sun.misc.Launcher.class
類的loader成員變量,而在上一篇文章中已經知道該loader的值就是應用類加載器(系統類加載器)System ClassLoader
。至此Tomcat中類加載機制和JDK的類加載機制也建立上了聯繫
現在Tomcat的類加載機制已完成了一大半,剩下用於加載每個web應用的類加載器WebApp ClassLoader
的分析,這個時候需要重新回到代碼清單1中看註釋(2)以下的部分,其主要做的事情是通過反射創建了org.apache.catalina.startup.Catalina
類的實例,然後調用了簽名爲void setParentClassLoader(ClassLoader parentClassLoader)
的方法,並傳入了Shared ClassLoader
,上面我們說過默認情況下Shared ClassLoader
就是Common ClassLoader
,因此其傳入的參數實際上是Common ClassLoader
我們思考既然有保存parent的方法,必定使用時會調用獲得parent方法,那麼我們需要查看Catalina
類中ClassLoader getParentClassLoader()
方法的調用棧(層級關係比較複雜,要緊跟主線不要迷失),最終定位到StandardContext
中的synchronized void startInternal() throws LifecycleException
方法(其中涉及到Tomcat的各種組件關係,生命週期管理等內容,將在下次分析Tomcat組件文章中詳細介紹),下面是隻保留核心邏輯的startInternal()
方法 代碼清單4
protected synchronized void startInternal() throws LifecycleException {
// 其他邏輯略......
// Add missing components as necessary
if (webappResources == null) { // (1) Required by Loader
if (log.isDebugEnabled())
log.debug("Configuring default Resources");
try {
String docBase = getDocBase();
if (docBase == null) {
setResources(new EmptyDirContext());
} else if (docBase.endsWith(".war")
&& !(new File(getBasePath())).isDirectory()) {
setResources(new WARDirContext());
} else {
setResources(new FileDirContext());
}
} catch (IllegalArgumentException e) {
log.error(sm.getString("standardContext.resourcesInit"), e);
ok = false;
}
}
if (ok) {
if (!resourcesStart()) {
throw new LifecycleException("Error in resourceStart()");
}
}
// (1) 爲每一個web應用創建一個WebappLoader
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
// 其他邏輯略......
try {
if (ok) {
// (2) 調用WebappLoader的start
// Start our subordinate components, if any
if ((loader != null) && (loader instanceof Lifecycle))
((Lifecycle) loader).start();
}
// 其他邏輯省略......
} finally {
// Unbinding thread
unbindThread(oldCCL);
}
}
(1)處註釋下的代碼邏輯就是爲每一個web應用創建一個類加載器,該類加載器的父類加載器就是通過getParentClassLoader()
得到的Shared ClassLoader
(Common ClassLoader
),(2)處代碼調用了WebappLoader
的start
方法,繼續跟進
protected void startInternal() throws LifecycleException {
// 其他邏輯省略.....
try {
//創建類加載器關鍵方法
classLoader = createClassLoader();
classLoader.setResources(container.getResources());
classLoader.setDelegate(this.delegate);
classLoader.setSearchExternalFirst(searchExternalFirst);
if (container instanceof StandardContext) {
classLoader.setAntiJARLocking(
((StandardContext) container).getAntiJARLocking());
classLoader.setClearReferencesRmiTargets(
((StandardContext) container).getClearReferencesRmiTargets());
classLoader.setClearReferencesStatic(
((StandardContext) container).getClearReferencesStatic());
classLoader.setClearReferencesStopThreads(
((StandardContext) container).getClearReferencesStopThreads());
classLoader.setClearReferencesStopTimerThreads(
((StandardContext) container).getClearReferencesStopTimerThreads());
classLoader.setClearReferencesHttpClientKeepAliveThread(
((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());
}
// 其他邏輯省略.....
}
由於Tomcat的設計,WebappLoader
的start
方法實際上調用的是父類的模板,而模板中的startinternal
方法由各個子類具體實現,其中最關鍵的方法爲createClassLoader()
圖5. WebappLoader中createClassLoader方法
上圖中的loadClass
成員變量的值爲org.apache.catalina.loader.WebappClassLoader
,所以,實際上該類爲每一個web應用創建了一個WebappClassLoader
的實例,該實例的parent就是Shared ClassLoader
或者Common ClassLoader
,至此WebApp ClassLoader
在圖1中的位置也得以驗證。
從理論上分析來看,由於類加載的“雙親委派”機制,一個類加載器只能加載本加載器指定的目錄以及使用有“繼承”關係的父類加載器加載過的類,而Tomcat爲每一個Web應用創建了一個WebappClassLoader
,不同的WebappClassLoader
是同級關係,不會存在交叉訪問的問題,從而達到web應用相互隔離的目的。
那Tomcat是否沒有"破壞"雙親委派機制呢?我們通過查看WebappClassLoader
及其父類WebappClassLoaderBase
的loadClass()
和findClass()
分析一下Tomcat加載web應用相關類的策略
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLockInternal(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
// Log access to stopped classloader
if (!started) {
try {
throw new IllegalStateException();
} catch (IllegalStateException e) {
log.info(sm.getString("webappClassLoader.stopped", name), e);
}
}
// (1)
// Check our previously loaded local class cache
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// (2)
// Check our previously loaded class cache
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// (3)
// Try loading the class with the system class loader, to prevent
// the webapp from overriding J2SE classes
try {
clazz = j2seClassLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
// Permission to access this class when using a 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;
if (name.endsWith("BeanInfo")) {
// BZ 57906: suppress logging for calls from
// java.beans.Introspector.findExplicitBeanInfo()
log.debug(error, se);
} else {
log.info(error, se);
}
throw new ClassNotFoundException(error, se);
}
}
}
// (4)
boolean delegateLoad = delegate || filter(name);
// (5)
// Delegate to our parent if requested
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
}
}
// (6)
// Search local repositories
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
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) {
// Ignore
}
// Delegate to parent unconditionally
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + 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
}
}
}
throw new ClassNotFoundException(name);
}
我們首先定位到WebappClassLoaderBase
的loadClass
方法,(1)處首先看name
對應的類是否存在緩存中,緩存是一個ConcurrentHashMap<String, ResourceEntry>
的集合,如果沒有緩存執行(2)處邏輯,從JVM中查找是否曾今加載過該類,(3)中的代碼保證自定義類不會覆蓋java基礎類庫中的類,(4)的邏輯就是是否進行雙親委派的分叉口,其中delegate
默認爲false
,那麼就要看filter(String)
方法,該方法的內部實際上將待加載類的全路徑名稱和一個成員變量protected static final String[] packageTriggers
中的類名進行比較,如果待加載的類名和packageTriggers
數組中的內容前綴匹配,則需要委派父類加載,即執行(5)處代碼,否則執行(6),調用重寫的findClass(String)
方法加載該類
public Class<?> findClass(String name) throws ClassNotFoundException {
// 其他代碼略去.....
// Ask our superclass to locate this class, if possible
// (throws ClassNotFoundException if it is not found)
Class<?> clazz = null;
try {
if (log.isTraceEnabled())
log.trace(" findClassInternal(" + name + ")");
// (1)
if (hasExternalRepositories && searchExternalFirst) {
try {
clazz = super.findClass(name);
} catch(ClassNotFoundException cnfe) {
// Ignore - will search internal repositories next
} catch(AccessControlException ace) {
log.warn("WebappClassLoaderBase.findClassInternal(" + name
+ ") security exception: " + ace.getMessage(), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
}
// (2)
if ((clazz == null)) {
try {
clazz = findClassInternal(name);
} catch(ClassNotFoundException cnfe) {
if (!hasExternalRepositories || searchExternalFirst) {
throw cnfe;
}
} catch(AccessControlException ace) {
log.warn("WebappClassLoaderBase.findClassInternal(" + name
+ ") security exception: " + ace.getMessage(), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
}
//其他代碼略去........
return (clazz);
}
(1)處由於hasExternalRepositories
和searchExternalFirst
默認爲false,因此執行(2)處邏輯,調用findClassInternal(String)
方法
圖6. WebappClassLoader類的findClassInternal方法
其主要的思想是根據待加載類的全路徑讀取該類的二進制數據,進而進行類的預定義、class source的解析等,將該類加載到JVM中
綜上所述,我認爲Tomcat的類加載機制不能算完全“正統”的雙親委派,WebappClassLoader
內部重寫了loadClass
和findClass
方法,實現了繞過“雙親委派”直接加載web應用內部的資源,當然可以通過在Context.xml文件中加上<Loader delegate = "true">
開啓正統的“雙親委派”加載機制
作者:寶之家
鏈接:https://www.jianshu.com/p/a18aecaecc89
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。