通過前面的文章我們知道,Tomcat的請求最終都會交給用戶配置的servlet實體來處理,Servlet類是配置在組態檔中的,這就需要類加載器對Servlet類進行加載,Tomcat容器自定義了類加載器,有以下特殊功能:1. 在載入類中指定某些規則;2.快取已經載入的類;3.實作類的預加載, 本文會對Tomcat的類加載器進行詳細介紹,
Java類加載雙親委派模型
Java類加載器是用戶程式和JVM虛擬機之間的橋梁,在Java程式中起了至關重要的作用,關于其詳細實作可以參考了java官方檔案關于虛擬機加載的教程,點此直達官方參考檔案,java中的類加載默認是采用雙親委派模型,即加載一個類時,首先判斷自身define加載器有沒有加載過此類,如果加載了直接獲取class物件,如果沒有查到,則交給加載器的父類加載器去重復上面程序,我在另外一篇文章中詳細介紹了Java的類加載機制,此處不做詳細介紹,

Loader介面
在載入Web應用程式中需要的servlet類及其相關類時要遵守一些明確的規則,例如應用程式中的servlet只能參考部署在WEB-INF/classes目錄及其子目錄下的類,但是,servlet類不能訪問其它路徑中的類,即使這些累包含在運行當前Tomcat的JVM的CLASSPATH環境變數中,此外,servlet類只能訪問WEB-INF/LIB目錄下的庫,其它目錄的類別庫均不能訪問,Tomcat中的載入器值得是Web應用程式載入器,而不僅僅是類載入器,載入器必須實作Loader介面,Loader介面的定義如下所示:
public interface Loader {
public void backgroundProcess();
public ClassLoader getClassLoader();
public Context getContext();
public void setContext(Context context);
public boolean getDelegate();
public void setDelegate(boolean delegate);
public void addPropertyChangeListener(PropertyChangeListener listener);
public boolean modified();
public void removePropertyChangeListener(PropertyChangeListener listener);
}
后臺任務:Loader介面需要進行在servlet類變更的時候實作類的重新加載,這個任務就是在backgroundProcess()中實作的,WebApploader中backgroundProcess()的實作如下所示,可以看到,當Context容器開啟了Reload功能并且倉庫變更的情況下,Loaders會先把類加載器設定為Web類加載器,重啟Context容器,重啟Context容器會重啟所有的子Wrapper容器,會銷毀并重新創建servlet類的實體,從而達到動態加載servlet類的目的,
@Override
public void backgroundProcess() {
Context context = getContext();
if (context != null) {
if (context.getReloadable() && modified()) {
ClassLoader originalTccl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(WebappLoader.class.getClassLoader());
context.reload();
} finally {
Thread.currentThread().setContextClassLoader(originalTccl);
}
}
}
}
類加載器:Loader的實作中,會使用一個自定義類載入器,它是WebappClassLoader類的一個實體,可以使用Loader介面的getClassLoader()方法來獲取Web載入器中的ClassLoader的實體,默認的類加載器的實作有兩種種:ParallelWebappClassLoader和WebappClassLoader
Context容器:Tomcat的載入器通常會與一個Context級別的servelt容器相關聯,Loader介面的getContainer()方法和setContainer()方法用來將載入器和某個servlet容器關聯,如果Context容器中的一個或者多個類被修改了,載入器也可以支持對類的多載,這樣,servlet程式員就可以重新編譯servlet類及其相關類,并將其重新載入而不需要重新啟動Tomcat,Loader介面使用modified()方法來支持類的自動多載,
類修改檢測:在載入器的具體實作中,如果倉庫中的一個或者多個類被修改了,那么modified()方法必須放回true,才能提供自動多載的支持
父載入器:載入器的實作會指明是否要委托給父類的載入器,可以通過setDelegate()和getDelegate方法配置,
WebappLoader類
Tomcat中唯一實作Loader介面的類就是WebappLoader類,其實體會用作Web應用容器的載入器,負責載入Web應用程式中所使用的類,在容器啟動的時候,WebApploader會執行以下作業:
- 創建類加載器
- 設定倉庫
- 設定類的路徑
- 設定訪問權限
- 啟動新執行緒來支持自動多載
創建類加載器
為了完成類加載功能,WebappLoader會按照配置創建類加載器的實體,Tomcat默認有兩種類加載器:WebappClassLoader和ParallelWebappClassLoader,默認情況下使用ParallelWebappClassLoader作為類加載器,用戶可以通過setLoaderClass()設定類加載器的名稱,WebappLoader創建類加載器的原始碼如下所示,我們可以看到類加載器的實體必須是WebappClassLoaderBase的子類,
private WebappClassLoaderBase createClassLoader()
throws Exception {
if (classLoader != null) {
return classLoader;
}
if (ParallelWebappClassLoader.class.getName().equals(loaderClass)) {
return new ParallelWebappClassLoader(context.getParentClassLoader());
}
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
ClassLoader parentClassLoader = context.getParentClassLoader();
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}
設定倉庫
WebappLoader會在啟動的時候呼叫類加載器的初始化方法,類加載器在初始化的時候會設定類加載的倉庫地址,默認的倉庫地址為"/WEB-INF/classes"和"/WEB-INF/lib",類加載器初始化原始碼如下所示:
@Override
public void start() throws LifecycleException {
state = LifecycleState.STARTING_PREP;
WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
for (WebResource classes : classesResources) {
if (classes.isDirectory() && classes.canRead()) {
localRepositories.add(classes.getURL());
}
}
WebResource[] jars = resources.listResources("/WEB-INF/lib");
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
localRepositories.add(jar.getURL());
jarModificationTimes.put(
jar.getName(), Long.valueOf(jar.getLastModified()));
}
}
state = LifecycleState.STARTED;
}
設定類路徑
設定類路徑是在初始化的時候呼叫setClassPath()方法完成的(原始碼如下),setClassPath()方法會在servlet背景關系中為Jasper JSP編譯器設定一個字串型別的屬性來指明類路徑資訊,此處不詳細介紹JSP相關內容,
private void setClassPath() {
// Validate our current state information
if (context == null)
return;
ServletContext servletContext = context.getServletContext();
if (servletContext == null)
return;
StringBuilder classpath = new StringBuilder();
// Assemble the class path information from our class loader chain
ClassLoader loader = getClassLoader();
if (delegate && loader != null) {
// Skip the webapp loader for now as delegation is enabled
loader = loader.getParent();
}
while (loader != null) {
if (!buildClassPath(classpath, loader)) {
break;
}
loader = loader.getParent();
}
if (delegate) {
// Delegation was enabled, go back and add the webapp paths
loader = getClassLoader();
if (loader != null) {
buildClassPath(classpath, loader);
}
}
this.classpath = classpath.toString();
// Store the assembled class path as a servlet context attribute
servletContext.setAttribute(Globals.CLASS_PATH_ATTR, this.classpath);
}
設定訪問權限
若是運行Tomcat的時候,使用了安全管理器,則setPermissions()方法會為類載入器設定訪問相關目錄的權限,比如只能訪問WEB-INF/classes和WEB-INF/lib的目錄,若是沒有使用安全管理器,則setPermissions()方法只是簡單地回傳,什么也不做,其原始碼如下:
/**
* Configure associated class loader permissions.
*/
private void setPermissions() {
if (!Globals.IS_SECURITY_ENABLED)
return;
if (context == null)
return;
// Tell the class loader the root of the context
ServletContext servletContext = context.getServletContext();
// Assigning permissions for the work directory
File workDir =
(File) servletContext.getAttribute(ServletContext.TEMPDIR);
if (workDir != null) {
try {
String workDirPath = workDir.getCanonicalPath();
classLoader.addPermission
(new FilePermission(workDirPath, "read,write"));
classLoader.addPermission
(new FilePermission(workDirPath + File.separator + "-",
"read,write,delete"));
} catch (IOException e) {
// Ignore
}
}
for (URL url : context.getResources().getBaseUrls()) {
classLoader.addPermission(url);
}
}
開啟新執行緒執行類的重新載入
WebappLoader類支持自動多載功能,如果WEB-INF/classes目錄或者WEB-INF/lib目錄下的某些類被重新編譯了,那么這個類會自動重新載入,而無需重啟Tomcat,為了實作此目的,WebappLoader類使用一個執行緒周期性的檢查每個資源的時間戳,間隔時間由變數checkInterval指定,單位為s,默認情況下,checkInterval的值為15s,每隔15s會檢查依次是否有檔案需要自動重新載入,頂層容器在啟動的時候,會啟動定時執行緒池回圈呼叫backgroundProcess任務,
protected void threadStart() {
if (backgroundProcessorDelay > 0
&& (getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState()))
&& (backgroundProcessorFuture == null || backgroundProcessorFuture.isDone())) {
if (backgroundProcessorFuture != null && backgroundProcessorFuture.isDone()) {
// There was an error executing the scheduled task, get it and log it
try {
backgroundProcessorFuture.get();
} catch (InterruptedException | ExecutionException e) {
log.error(sm.getString("containerBase.backgroundProcess.error"), e);
}
}
backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
.scheduleWithFixedDelay(new ContainerBackgroundProcessor(),
backgroundProcessorDelay, backgroundProcessorDelay,
TimeUnit.SECONDS);
}
}
@Override
public void backgroundProcess() {
Context context = getContext();
if (context != null) {
if (context.getReloadable() && modified()) {
ClassLoader originalTccl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(WebappLoader.class.getClassLoader());
context.reload();
} finally {
Thread.currentThread().setContextClassLoader(originalTccl);
}
}
}
}
WebappClassLoader類加載器
Web應用程式中負責載入類的類載入器有兩種:ParallelWebappClassLoader和WebappClassLoaderBase,二者實作大同小異,本節以WebappClassLoader類加載器為例,介紹Tomcat的類加載器,
WebappClassLoader的設計方案考慮了優化和安全兩方面,例如,它會快取之前已經載入的類來提升性能,還會快取加載失敗的類的名字,這樣,當再次請求加載同一個類的時候,類加載器就會直接拋出ClassNotFindException例外,而不是再次去查找這個類,WebappClassLoader會在倉庫串列和指定的JAR檔案中搜索需要在載入的類,
類快取
為了達到更好的性能,WebappClassLoader會快取已經載入的類,這樣下次再使用該類的時候,會直接從快取中獲取,由WebappClassLoader載入的類都會被視為資源進行快取,對應的類為“ResourceEntry”類的實體,ResourceEndty保存了其所代表的class檔案的位元組流、最后一次修改日期,Manifest資訊等,如下為類加載程序中讀取快取的部分代碼和ResourceEntry的定義原始碼,
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 省略部分邏輯
// (0) 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;
}
// 省略部分邏輯
}
protected Class<?> findLoadedClass0(String name) {
String path = binaryNameToPath(name, true);
ResourceEntry entry = resourceEntries.get(path);
if (entry != null) {
return entry.loadedClass;
}
return null;
}
public class ResourceEntry {
/**
* The "last modified" time of the origin file at the time this resource
* was loaded, in milliseconds since the epoch.
*/
public long lastModified = -1;
/**
* Loaded class.
*/
public volatile Class<?> loadedClass = null;
}
載入類
載入類的時候,WebappClassLoader要遵循如下規則:
- 因為所有已經載入的類都會快取起來,所以載入類的時候要先檢查本地快取,
- 若本地快取沒有,則檢查父類加載器的快取,呼叫ClassLoader介面的findLoadedClass()方法,
- 若兩個快取總都沒有,則使用系統類加載器進行加載,防止Web應用程式中的類覆寫J2EE中的類,
- 若啟用了SecurityManager,則檢查是否允許載入該類,若該類是禁止載入的類,拋出ClassNotFoundException例外,
- 若打開了標志位delegate,或者待載入的在類不能用web類加載器加載的類,則使用父類加載器來加載器來加載相關類,如果父類加載器為null,則使用系統類加載器,
- 從當前倉庫載入類,
- 當前倉庫沒有需要載入的類,而且delegate關閉,則是用父類載入器來載入相關的類,
- 若沒有找到需要加載的類,則拋出ClassNotFindException,
Tomcat類加載結構
Tomcat容器在啟動的時候會初始化類加載器,Tomcat的類加載器分為四種型別:Common類加載器,Cataline類加載器和Shared類加載器,此外每個應用都會有自己的Webapp類加載器,也就是我們上文介紹的WebappClassLoader,四者之間的關系如下所示,

Common類加載器,Cataline類加載器和Shared類加載器會在Tomcat容器啟動的時候就初始化完成,初始化代碼如下所示:
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);
}
}
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
String value = https://www.cnblogs.com/yuhushen/archive/2021/10/06/CatalinaProperties.getProperty(name +".loader");
if ((value =https://www.cnblogs.com/yuhushen/archive/2021/10/06/= null) || (value.equals("")))
return parent;
value = https://www.cnblogs.com/yuhushen/archive/2021/10/06/replace(value);
List 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
}
// 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));
}
}
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
而Webapp類加載器則是在Context容器啟動時候有WebappLoader初始化,Webapp類加載器的父類加載器是Tomcat容器在初始化階段通過反射設定的,反射設定父類加載器的原始碼如下所示:
public void init() throws Exception {
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");
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().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;
}
Tomcat類加載結構的目的
- 一個web容器可能需要部署兩個應用程式,不同的應用程式可能會依賴同一個第三方類別庫的不同版本,不能要求同一個類別庫在同一個服務器只有一份,因此要保證每個應用程式的類別庫都是獨立的,保證相互隔離,所以每個應用需要自身的Webapp類加載器,
- 部署在同一個web容器中相同的類別庫相同的版本可以共享,否則,如果服務器有10個應用程式,那么要有10份相同的類別庫加載進虛擬機,所以需要Shared類加載器
- web容器也有自己依賴的類別庫,不能于應用程式的類別庫混淆,基于安全考慮,應該讓容器的類別庫和程式的類別庫隔離開來,所以需要Cataline類加載器,
- web容器要支持jsp的修改,我們知道,jsp 檔案最終也是要編譯成class檔案才能在虛擬機中運行,但程式運行后修改jsp已經是司空見慣的事情,否則要你何用? 所以,web容器需要支持 jsp 修改后不用重啟,
還有最后一個類的共享的問題,如果十個web應用都引入了spring的類,由于web類加載器的隔離,那么對記憶體的開銷是很大的,此時我們可以想到shared類加載器,我們肯定都會選擇將spring的jar放于shared目錄底下,但是此時又會存在一個問題,shared類加載器是webapp類加載器的parent,若spring中的getBean方法需要加載web應用底下的類,這種程序是違反雙親委托機制的,
打破雙親委托機制的桎梏:執行緒背景關系類加載器執行緒背景關系類加載器是指的當前執行緒所用的類加載器,可以通過Thread.currentThread().getContextClassLoader()獲得或者設定,在spring中,他會選擇執行緒背景關系類加載器去加載web應用底下的類,如此就打破了雙親委托機制,
參考檔案串列
- tomcat學習|tomcat中的類加載器
- 深入理解Tomcat(五)類加載機制
我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd

本文最先發布至微信公眾號,著作權所有,禁止轉載!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/305842.html
標籤:其他
