學習Tomcat(六)之類載入器
通過前面的文章我們知道,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 = 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
}
// 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應用底下的類,如此就打破了雙親委託機制。
參考文檔列表
我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd
本文最先發布至微信公眾號,版權所有,禁止轉載!