深入剖析Tomcat(八) 載入器與打破雙親委派機制的自定義類加載器

寫這篇文章讓我頭大了好幾天,書中描述的內容倒是不多,可能也是那會Tomcat的現狀。如今Tomcat發展了好多代,加上springboot的廣泛應用,導致現在的類加載的步驟和Tomcat資料中描述的大相徑庭。可能也是由于微服務的發展,導致Tomcat中原來可以部署多個應用的特性正在淡化,Tomcat與springboot結合后的啟動流程精簡了許多。下面我將我了解到的知識介紹一下,有不對的請指出,有新發現我也會繼續補充。

進入正文

首先,搞清楚兩個詞語的含義,載入器與類加載器。實現了org.apache.catalina.Loader接口的類為載入器,容器直接持有的是載入器,比如我們前面提到的容器基礎類?ContainerBase 中持有一個?Loader 對象的引用。類加載器被載入器持有,作為載入器的一個屬性存在。

載入器

這是Loader接口的定義

package org.apache.catalina;import java.beans.PropertyChangeListener;public interface Loader {// 持有一個類加載器public ClassLoader getClassLoader();// 與此載入器關聯的容器對象public Container getContainer();public void setContainer(Container container);// 存儲 Host 在創建 Context 時將使用的默認配置public DefaultContext getDefaultContext();public void setDefaultContext(DefaultContext defaultContext);// 配合類加載器使用,設置類加載的流程是否遵循雙親委派機制public boolean getDelegate();public void setDelegate(boolean delegate);public String getInfo();// 是否支持熱部署(動態重新加載類)public boolean getReloadable();public void setReloadable(boolean reloadable);// --------------------------------------------------------- Public Methods// 設置屬性變化監聽器public void addPropertyChangeListener(PropertyChangeListener listener);public void removePropertyChangeListener(PropertyChangeListener listener);// 添加一個類加載器的存儲庫(該載入器持有的類加載器只能去指定的存儲庫中加載類)public void addRepository(String repository);public String[] findRepositories();// 與此載入器關聯的內部庫中是否有類文件被修改了?這將會成為是否需要重新加載類(啟動熱部署)的依據public boolean modified();}

由此接口的定義可以看出:載入器大概有兩大功能

  1. 持有一個類加載器,并管理類加載器的一些行為:是否遵循雙親委派機制、包含哪些類庫
  2. 擁有熱部署的功能,并可設定是否開啟。開啟后,當class文件或jar包發生更改時,會觸發自動重載操作

Tomcat的載入器通常會與一個Context級別的servlet容器相關聯,而Context容器代表的是一個Web應用程序,所以載入器的作用就是加載這個Web應用程序下的類,并支持自動重載(當然也可以關閉自動重載)。

類加載器

類加載器其實是jdk中定義的東西,繼承了java.lang.ClassLoader的類叫做類加載器。jdk中關于類加載器的實現有很多,Tomcat主要使用URLClassLoader這個類加載器,URLClassLoader可以通過url的形式添加該類加載器的存儲庫,該類加載器只會去它的存儲庫中查找并加載類。

了解過JVM類加載的同學應該都知道雙親委派機制。這里再簡單闡述下這個機制。

雙親委派機制

jdk定義了三種類加載器

  • 啟動類加載器(Bootstrap Class Loader ):負責加載<JAVA_HOME>\lib目錄中?rt.jar、tools.jar?等包中的基礎類
  • 擴展類加載器(Extension Class Loader):負責加載<JAVA_HOME>\lib\ext目錄中的類
  • 應用程序類加載器(Application Class Loader):也稱系統類加載器,system class loader ,它負責加載用戶類路徑(ClassPath)上的所有類,如果應用程序中沒有自定義類加載器的話,一般情況下這個就是程序默認的類加載器。

用戶也可以自定義類加載器,以實現類庫的隔離與重載。

類加載器的雙親委派模型如上圖,每個類加載器在收到類加載請求時,都會優先交給它的父類加載器來執行,直到這個加載請求到達啟動類加載器,如果父類加載器加載不了,自己才會嘗試加載。這種父類優先的模式就形成了如下圖所示的這種規則(假如當前場景是應用程序類加載器要加載一個類)

這種機制保證了JDK中的基礎類,如java.lang.Object,java.lang.String等永遠只會被啟動類加載器加載,不會被用戶在項目中編寫的同包同名類影響。

另外,JVM中判斷一個Class唯一性的標準為:加載該Class的類加載器+該Class本身。所以雙親委派機制也維護了這一標準,同一個類不會被不同的類加載器加載。

至于為什么叫雙親委派,我想大概是因為jdk中只定義了三種類加載器,而我們編寫的工程類一般使用“應用程序類加載器”,在這個類加載器上面還有兩代親,所以就叫“雙親委派”了。

Tomcat的自定義類加載器

Tomcat在誕生之處就實現了一個目標:我不僅是一個Web服務,我還是一個支持部署多個應用的Web服務。Tomcat有一個webapps目錄,這個目錄下要放的就是我們的項目應用,可以放多個應用,客戶端訪問時帶上應用名就能訪問到指定應用。

這個多應用的支持就對jdk的類加載機制帶來了挑戰,不同的應用可能會有同包同名的類,或者引用了一個第三方包的不同版本。如果所有應用的類都用系統類加載器來加載的話,那么肯定就會產生沖突了,所以需要一個機制,將不同應用的類加載過程隔離開來,因此,Tomcat自定義了自己的類加載器,來實現自己自定義的類加載邏輯。

早期的Tomcat的目錄結構是下面這樣的,(我下載了一個5.0.28版本的Tomcat工程)

我圈起來的四個包,Tomcat用了四個類加載器來分別加載

  • common目錄下的類庫可以被Tomcat和所有Web應用共同使用,使用Common類加載器來加載。
  • server目錄下的類庫可被Tomcat使用,對所有Web應用不可見,使用Catalina類加載器(server類加載器)來加載。
  • shared目錄下的類庫可被所有Web應用使用,對Tomcat自己不可見,使用 Shared類加載加載。
  • webapps目錄下放了1到多個應用目錄,每個應用目錄的類庫僅對該應用自己可見,每個應用使用一個WebApp類加載器實例來加載自己的類。

所以這四個自定義類加載器結合jdk的三個類加載器就形成了這個關系

不同的類加載器分別加載各自目錄下的類庫,也就將各個類庫隔離開來了。

隨著Tomcat的發展,在升級到版本6后,它默認的目錄結構發生了變化(我下載了一個6.0.0版本的Tomcat工程)

common、server、shared三個目錄沒有了,多了一個lib目錄,這個lib目錄的作用相當于之前common目錄的作用,用Common類加載器進行加載。Tomcat這么做說明它也發現了用戶實際使用過程中,對server和shared的使用場景很少,所以干脆將它們從默認配置中移除了。如果我升級到了Tomcat高版本,還想用server或shared目錄怎么辦?那就自己在catalina.properties文件中配置 server.loader與shared.loader兩個屬性的屬性值即可

由于少了server與shared兩個目錄,所以默認情況下Tomcat的類加載器的關系又成了這樣

了解了Tomcat的載入器與類加載器的情況,現在來看看它們的代碼實現

載入器的代碼實現

Tomcat提供的載入器實現類為?org.apache.catalina.loader.WebappLoader。

WebappLoader類中創建了一個自定義類加載器(org.apache.catalina.loader.WebappClassLoader類的實例)。

WebappLoader類實現了Runnable接口,并開啟一個線程來支持類的自動重載。

WebappLoader類實現了Lifecycle接口,它的start()方法中的主要邏輯為

  • 創建一個類載入器;
  • 設置倉庫;
  • 設置類路徑;
  • 設置訪問權限;
  • 啟動一個新線程來支持自動重載。

接下來分別說一下start方法中的這幾件事

創建一個類載入器

WebappLoader中有一個loaderClass屬性,來存放類加載器的全限定名,默認是org.apache.catalina.loader.WebappClassLoader,也支持修改。

WebappLoader根據loaderClass來反射創建一個類加載器。

private String loaderClass = "org.apache.catalina.loader.WebappClassLoader";

設置倉庫

如果載入器關聯的Context容器需要引入額外的類庫作為該應用的類庫,則將額外的類庫設置到WebappLoader的?repositories 屬性中(通過addRepository方法),在start()方法中 會首先將repositories中的類庫添加到類加載器中,然后調用?setRepositories() 方法將WEB-INF/classes 與 WEB-INF/lib兩個類庫添加到類加載器中。

private String[] repositories = new String[0];

設置類路徑

這是jsp相關的內容,考慮到jsp基本已經不再使用,所以這里不再研究。

設置訪問權限

訪問權限的內容將在第十章進行介紹,這些不做研究。

啟動一個新線程來支持自動重載

WebappLoader類實現了Runnable接口,在 start 方法中,會啟動這個線程,用來不斷檢查該載入器對應的Context容器中的類(也就是應用程序中的類)是否發生了變更,如果發生了變更則要通知Context進行類重載。

其中檢查類是否發生變更,是調用的類加載器的modified()方法,邏輯由類加載器實現。

檢查到類發生變更后,Tomcat另起了一個線程來調用Context中的reload方法。至于為什么要另起一個線程,我暫時沒有理解,如果你知道,還請評論賜教。

下面是WebappLoader的代碼,我省略了一些代碼,保留了主要邏輯代碼

public class WebappLoader implements Lifecycle, Loader, PropertyChangeListener, Runnable {// ----------------- 構造方法public WebappLoader() {this(null);}public WebappLoader(ClassLoader parent) {super();this.parentClassLoader = parent;}// 熱部署線程,巡檢的時間間隔private int checkInterval = 15;// 類加載器private WebappClassLoader classLoader = null;private Container container = null;protected DefaultContext defaultContext = null;// 是否遵循雙親委派模型private boolean delegate = false;protected LifecycleSupport lifecycle = new LifecycleSupport(this);// 類加載器的全限定名private String loaderClass = "org.apache.catalina.loader.WebappClassLoader";// classLoader的父 類加載器private ClassLoader parentClassLoader = null;// 是否支持熱重載private boolean reloadable = false;// 類加載器關聯的類庫,這是除了WEB-INF/classes與WEB-INF/lib外的額外類庫private String repositories[] = new String[0];private boolean started = false;protected PropertyChangeSupport support = new PropertyChangeSupport(this);// 熱部署巡檢線程private Thread thread = null;// 巡檢線程是否停止了private boolean threadDone = false;// 巡檢線程的nameprivate String threadName = "WebappLoader";// ------------------ Propertiespublic void setReloadable(boolean reloadable) {// Process this property changeboolean oldReloadable = this.reloadable;this.reloadable = reloadable;support.firePropertyChange("reloadable", new Boolean(oldReloadable), new Boolean(this.reloadable));// Start or stop our background thread if requiredif (!started) return;if (!oldReloadable && this.reloadable) threadStart();else if (oldReloadable && !this.reloadable) threadStop();}// ------------------ Public Methods/*** Has the internal repository associated with this Loader been modified,* such that the loaded classes should be reloaded?*/public boolean modified() {return (classLoader.modified());}// ------------------ Lifecycle Methodspublic void start() throws LifecycleException {if (started) {throw new LifecycleException(sm.getString("webappLoader.alreadyStarted"));}lifecycle.fireLifecycleEvent(START_EVENT, null);started = true;if (container.getResources() == null) {return;}try {// 創建類加載器classLoader = createClassLoader();classLoader.setResources(container.getResources());classLoader.setDebug(this.debug);classLoader.setDelegate(this.delegate);// 如果載入器額外設置了類庫,則將這些類庫添加到類加載器的類庫中for (int i = 0; i < repositories.length; i++) {classLoader.addRepository(repositories[i]);}// 設置倉庫,WEB_INF/classes 與 WEB_INF/lib 兩個目錄setRepositories();// 設置類路徑,與JSP相關,不再研究setClassPath();// 設置訪問權限,這塊內容后面章節再介紹setPermissions();if (classLoader instanceof Lifecycle) {((Lifecycle) classLoader).start();}// Binding the Webapp class loader to the directory contextDirContextURLStreamHandler.bind((ClassLoader) classLoader, this.container.getResources());} catch (Throwable t) {throw new LifecycleException("start: ", t);}// 驗證所有必需的包都是實際可用的,這個方法這里暫不研究validatePackages();// 如果支持重載的話,開啟一個守護線程來巡檢,在線程內完成重載的觸發流程if (reloadable) {log(sm.getString("webappLoader.reloading"));try {threadStart();} catch (IllegalStateException e) {throw new LifecycleException(e);}}}public void stop() throws LifecycleException {// Validate and update our current component stateif (!started) throw new LifecycleException(sm.getString("webappLoader.notStarted"));lifecycle.fireLifecycleEvent(STOP_EVENT, null);started = false;// Stop our background thread if we are reloadableif (reloadable) threadStop();// Remove context attributes as appropriateif (container instanceof Context) {ServletContext servletContext = ((Context) container).getServletContext();servletContext.removeAttribute(Globals.CLASS_PATH_ATTR);}// Throw away our current class loaderif (classLoader instanceof Lifecycle) ((Lifecycle) classLoader).stop();DirContextURLStreamHandler.unbind((ClassLoader) classLoader);classLoader = null;}// ------------------------------------------------------- Private Methods/*** 創建一個類加載器*/private WebappClassLoader createClassLoader() throws Exception {Class clazz = Class.forName(loaderClass);WebappClassLoader classLoader = null;// 根據 parentClassLoader 有沒有值來使用 WebappClassLoader 的不同構造函數if (parentClassLoader == null) {classLoader = (WebappClassLoader) clazz.newInstance();} else {Class[] argTypes = {ClassLoader.class};Object[] args = {parentClassLoader};Constructor constr = clazz.getConstructor(argTypes);classLoader = (WebappClassLoader) constr.newInstance(args);}return classLoader;}/*** 基于關聯的Context,為我們的類加載器配置類庫。主要是 WEB-INF/classes 和 WEB-INF/lib 兩個目錄*/private void setRepositories() {if (!(container instanceof Context)) return;ServletContext servletContext = ((Context) container).getServletContext();if (servletContext == null) return;// Loading the work directoryFile workDir = (File) servletContext.getAttribute(Globals.WORK_DIR_ATTR);if (workDir == null) return;log(sm.getString("webappLoader.deploy", workDir.getAbsolutePath()));DirContext resources = container.getResources();// Setting up the class repository (/WEB-INF/classes), if it existsString classesPath = "/WEB-INF/classes";DirContext classes = null;try {Object object = resources.lookup(classesPath);if (object instanceof DirContext) {classes = (DirContext) object;}} catch (NamingException e) {// Silent catch: it's valid that no /WEB-INF/classes collection// exists}if (classes != null) {File classRepository = null;String absoluteClassesPath = servletContext.getRealPath(classesPath);if (absoluteClassesPath != null) {classRepository = new File(absoluteClassesPath);} else {classRepository = new File(workDir, classesPath);classRepository.mkdirs();copyDir(classes, classRepository);}log(sm.getString("webappLoader.classDeploy", classesPath, classRepository.getAbsolutePath()));// Adding the repository to the class loaderclassLoader.addRepository(classesPath + "/", classRepository);}// Setting up the JAR repository (/WEB-INF/lib), if it existsString libPath = "/WEB-INF/lib";classLoader.setJarPath(libPath);DirContext libDir = null;// Looking up directory /WEB-INF/lib in the contexttry {Object object = resources.lookup(libPath);if (object instanceof DirContext) libDir = (DirContext) object;} catch (NamingException e) {// Silent catch: it's valid that no /WEB-INF/lib collection// exists}if (libDir != null) {boolean copyJars = false;String absoluteLibPath = servletContext.getRealPath(libPath);File destDir = null;if (absoluteLibPath != null) {destDir = new File(absoluteLibPath);} else {copyJars = true;destDir = new File(workDir, libPath);destDir.mkdirs();}// Looking up directory /WEB-INF/lib in the contexttry {NamingEnumeration myEnum = resources.listBindings(libPath);while (myEnum.hasMoreElements()) {Binding binding = (Binding) myEnum.nextElement();String filename = libPath + "/" + binding.getName();if (!filename.endsWith(".jar")) continue;// Copy JAR in the work directory, always (the JAR file// would get locked otherwise, which would make it// impossible to update it or remove it at runtime)File destFile = new File(destDir, binding.getName());log(sm.getString("webappLoader.jarDeploy", filename, destFile.getAbsolutePath()));Resource jarResource = (Resource) binding.getObject();if (copyJars) {if (!copy(jarResource.streamContent(), new FileOutputStream(destFile))) continue;}JarFile jarFile = new JarFile(destFile);classLoader.addJar(filename, jarFile, destFile);}} catch (NamingException e) {// Silent catch: it's valid that no /WEB-INF/lib directory// exists} catch (IOException e) {e.printStackTrace();}}}// 開啟一個巡檢線程private void threadStart() {// Has the background thread already been started?if (thread != null) return;// Validate our current stateif (!reloadable) throw new IllegalStateException(sm.getString("webappLoader.notReloadable"));if (!(container instanceof Context)) throw new IllegalStateException(sm.getString("webappLoader.notContext"));// Start the background threadthreadDone = false;threadName = "WebappLoader[" + container.getName() + "]";thread = new Thread(this, threadName);thread.setDaemon(true);thread.start();}// 讓當前線程睡一會private void threadSleep() {try {Thread.sleep(checkInterval * 1000L);} catch (InterruptedException e) {;}}// 停止巡檢線程private void threadStop() {if (thread == null) return;threadDone = true;thread.interrupt();try {thread.join();} catch (InterruptedException e) {;}thread = null;}// 巡檢線程的run方法public void run() {// 循環檢查,直到 threadDone 為 truewhile (!threadDone) {// 睡一會再檢查threadSleep();if (!started) break;try {// 檢查類是否被更改過if (!classLoader.modified()) {continue;} } catch (Exception e) {log(sm.getString("webappLoader.failModifiedCheck"), e);continue;}// 檢查到類被更改過,通知Context去重載類notifyContext();break;}}/*** 另外開啟一個線程來通知Context容器需要進行類重載了*/private void notifyContext() {WebappContextNotifier notifier = new WebappContextNotifier();(new Thread(notifier)).start();}// -------------------- WebappContextNotifier 內部類/*** 私有線程類來通知關聯Context,需要重新加載類了。*/protected class WebappContextNotifier implements Runnable {public void run() {// 類重載的邏輯實際在Context容器類中((Context) container).reload();}}}

自定義類加載器的代碼實現

Tomcat為Web應用程序做的類加載器為org.apache.catalina.loader.WebappClassLoader類的實例,WebappClassLoader繼承自URLClassLoader。

WebappClassLoader中主要是對類加載的邏輯做了自定義,用來隔離各個Web應用的類庫。同時它也做了一些緩存,來提升類加載的效率。

考慮到安全性,WebappClassLoader 類不允許載入指定的某些類,這些類的名字存儲在一個字符串數組變量triggers 中,當前只有一個元素?

此外,某些特殊的包及其子包下的類也是不允許WebApp類加載器直接加載的,需要先委托父類加載器去加載。

每個由 WebappClassLoader載入的類(無論是在WEB-INF/classes 目錄下還是從某個JAR文件內作為類文件部署 ), 都視為“資源”。資源是 org.apache.catalina.loader.ResourceEntry類的實 例 。ResourceEntry 實例會保存其所代表的class 文件的字節流、最后一次修改日期、Manifest 信息(如果資源來自與一個JAR 文件的話)等。

為了達到更好的新能,WebappClassLoader會緩存已經加載過的類,放到?resourceEntries 這個map中。

protected HashMap<String,ResourceEntry> resourceEntries = new HashMap<>();

另外,如果WebappClassLoader在嘗試加載某類時,沒有找到此類并報了ClassNotFoundException,那么WebappClassLoader也會將這個不存在的類記錄下來,下次再需要加載時直接拋異常就行。

protected HashMap<String,String> notFoundResources = new HashMap<>();

本章應用程序中沒有定義common類加載類,所以本章代碼中類加載器的結構如下

WebappClassLoader中有兩個類加載器屬性

parent是它的父類加載器,正常來說這個值應該是common類加載器,但是我們本次代碼并沒有創建common類加載器,所以parent屬性為null。

sytem就是系統類加載器,也就是應用程序類加載器。

WebappClassLoader類重寫了loadClass方法,自定義的類加載邏輯如下圖

WebappClassLoader在加載類時會首先委托應用程序類加載器去加載,應用程序類加載器能加載哪些類呢?

應用程序類加載器默認加載以下目錄和文件中的類:

1.通過 -classpath 或 -cp 參數指定的路徑:
當啟動Java應用程序時,可以使用 -classpath 或 -cp 參數來指定一個或多個目錄和JAR文件,作為類的搜索路徑。
例如:java -classpath /path/to/classes:/path/to/lib/some-library.jar com.example.Main
2.環境變量 CLASSPATH 指定的路徑:
如果沒有使用 -classpath 或 -cp 參數,系統類加載器會使用環境變量 CLASSPATH 中指定的路徑。
例如,CLASSPATH=/path/to/classes:/path/to/lib/some-library.jar
3.當前工作目錄:
如果 CLASSPATH 沒有指定,系統類加載器會默認包含當前工作目錄(.),即應用程序啟動時的工作目錄。

在通過WebappClassLoader加載的一個應用程序中的類中,如果依賴了其他類,這些其他類也會通過WebappClassLoader來加載,后面我會通過一個MyObject的例子來驗證。

WebappClassLoader 中含有 modified() 方法,用來判斷該類加載器對應的類庫中有沒有class文件或jar包被修改了。這個方法會被WebappLoader中的熱加載巡檢線程不斷調用。

下面是WebappClassLoader的部分代碼,完整代碼請看源碼

public class WebappClassLoader extends URLClassLoader implements Reloader, Lifecycle {private static final String[] triggers = {"javax.servlet.Servlet"                     // Servlet API};private static final String[] packageTriggers = {"javax",                                     // Java extensions"org.xml.sax",                               // SAX 1 & 2"org.w3c.dom",                               // DOM 1 & 2"org.apache.xerces",                         // Xerces 1 & 2"org.apache.xalan"                           // Xalan};public WebappClassLoader() {super(new URL[0]);this.parent = getParent();system = getSystemClassLoader();}public WebappClassLoader(ClassLoader parent) {super(new URL[0], parent);this.parent = getParent();system = getSystemClassLoader();}// 緩存已經加載過的類protected HashMap<String,ResourceEntry> resourceEntries = new HashMap<>();// 緩存找不到的那些類的類名protected HashMap<String,String> notFoundResources = new HashMap<>();// 是否遵循雙親委派模型protected boolean delegate = false;// 該類加載器的類庫protected String[] repositories = new String[0];// 該類加載器的jar包類庫protected JarFile[] jarFiles = new JarFile[0];// jar包名字的集合protected String[] jarNames = new String[0];// jar包類庫中各jar包的最后修改日期protected long[] lastModifiedDates = new long[0];// modify方法需要檢查的所有資源路徑protected String[] paths = new String[0];// 父 類加載器private ClassLoader parent = null;// 系統類加載器private ClassLoader system = null;public Class loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {Class clazz = null;// Don't load classes if class loader is stoppedif (!started) {log("Lifecycle error : CL stopped");throw new ClassNotFoundException(name);}// (0) 檢查該類加載器的緩存中存在clazz = findLoadedClass0(name);if (clazz != null) {if (resolve) {resolveClass(clazz);}return clazz;}// (0.1) 檢查JVM提供的類加載緩存中是否存在clazz = findLoadedClass(name);if (clazz != null) {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) Permission to access this class when using a SecurityManagerif (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;System.out.println(error);se.printStackTrace();log(error);throw new ClassNotFoundException(error);}}}boolean delegateLoad = delegate || filter(name);// (1) 如果遵循雙親委派機制的話,要先交給父類加載器去加載if (delegateLoad) {ClassLoader loader = parent;if (loader == null) loader = system;try {clazz = loader.loadClass(name);if (clazz != null) {if (debug >= 3) log("  Loading class from parent");if (resolve) resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {;}}// (2) WebappClassLoader自己加載,從自己的類庫中加載try {clazz = findClass(name);if (clazz != null) {if (debug >= 3) log("  Loading class from local repository");if (resolve) resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {;}// (3) 強制使用父類加載器進行加載if (!delegateLoad) {if (debug >= 3) log("  Delegating to parent classloader");ClassLoader loader = parent;if (loader == null) loader = system;try {clazz = loader.loadClass(name);if (clazz != null) {if (debug >= 3) log("  Loading class from parent");if (resolve) resolveClass(clazz);return (clazz);}} catch (ClassNotFoundException e) {;}}// This class was not foundthrow new ClassNotFoundException(name);}public Class findClass(String name) throws ClassNotFoundException {if (debug >= 3) {log("    findClass(" + name + ")");}// (1) Permission to define this class when using a SecurityManagerif (securityManager != null) {int i = name.lastIndexOf('.');if (i >= 0) {try {if (debug >= 4) log("      securityManager.checkPackageDefinition");securityManager.checkPackageDefinition(name.substring(0, i));} catch (Exception se) {if (debug >= 4) log("      -->Exception-->ClassNotFoundException", se);throw new ClassNotFoundException(name);}}}// Ask our superclass to locate this class, if possible// (throws ClassNotFoundException if it is not found)Class clazz = null;try {if (debug >= 4) log("      findClassInternal(" + name + ")");try {clazz = findClassInternal(name);} catch (ClassNotFoundException cnfe) {if (!hasExternalRepositories) {throw cnfe;}} catch (AccessControlException ace) {ace.printStackTrace();throw new ClassNotFoundException(name);} catch (RuntimeException e) {if (debug >= 4) log("      -->RuntimeException Rethrown", e);throw e;}if ((clazz == null) && hasExternalRepositories) {try {clazz = super.findClass(name);} catch (AccessControlException ace) {throw new ClassNotFoundException(name);} catch (RuntimeException e) {if (debug >= 4) log("      -->RuntimeException Rethrown", e);throw e;}}if (clazz == null) {if (debug >= 3) log("    --> Returning ClassNotFoundException");throw new ClassNotFoundException(name);}} catch (ClassNotFoundException e) {if (debug >= 3) log("    --> Passing on ClassNotFoundException", e);throw e;}// Return the class we have locatedif (debug >= 4) log("      Returning class " + clazz);if ((debug >= 4) && (clazz != null)) log("      Loaded by " + clazz.getClassLoader());return (clazz);}/*** 添加一個類庫到類加載器的類庫集合中,該類加載器將加載這些類庫中的類。* 這個方法只接受一個參數,即類庫的路徑名。它的作用是向 WebappClassLoader 中添加一個新的類庫路徑;* 這個方法假設類庫路徑指向的是一個有效的 URL,并且不進行文件存在性檢查。*/public void addRepository(String repository) {// 忽略標準庫,他們已經被其他方法加載過了if (repository.startsWith("/WEB-INF/lib") || repository.startsWith("/WEB-INF/classes")) {return;} // Add this repository to our underlying class loadertry {URL url = new URL(repository);super.addURL(url);hasExternalRepositories = true;} catch (MalformedURLException e) {throw new IllegalArgumentException(e.toString());}}/*** 同addRepository(String repository),第二個參數file是類庫的絕對路徑file* 這個方法接受兩個參數,第一個參數是類庫的路徑名,第二個參數是表示類庫文件的 File對象。與第一個方法不同的是,這個方法會將類庫路徑名和對應的文件對象一一對應地添加到內部的數組中。這種方式更為靈活,因為它可以將路徑名和文件對象關聯起來,便于后續的管理和使用。*/synchronized void addRepository(String repository, File file) {if (repository == null) return;int i;// Add this repository to our internal listString[] result = new String[repositories.length + 1];for (i = 0; i < repositories.length; i++) {result[i] = repositories[i];}result[repositories.length] = repository;repositories = result;// Add the file to the listFile[] result2 = new File[files.length + 1];for (i = 0; i < files.length; i++) {result2[i] = files[i];}result2[files.length] = file;files = result2;}/*** 將一個jar包加到類庫中*/synchronized void addJar(String jar, JarFile jarFile, File file) throws IOException {if (jar == null) return;if (jarFile == null) return;if (file == null) return;int i;if ((jarPath != null) && (jar.startsWith(jarPath))) {String jarName = jar.substring(jarPath.length());while (jarName.startsWith("/")) jarName = jarName.substring(1);String[] result = new String[jarNames.length + 1];for (i = 0; i < jarNames.length; i++) {result[i] = jarNames[i];}result[jarNames.length] = jarName;jarNames = result;}try {// Register the JAR for trackinglong lastModified = ((ResourceAttributes) resources.getAttributes(jar)).getLastModified();String[] result = new String[paths.length + 1];for (i = 0; i < paths.length; i++) {result[i] = paths[i];}result[paths.length] = jar;paths = result;long[] result3 = new long[lastModifiedDates.length + 1];for (i = 0; i < lastModifiedDates.length; i++) {result3[i] = lastModifiedDates[i];}result3[lastModifiedDates.length] = lastModified;lastModifiedDates = result3;} catch (NamingException e) {// Ignore}// If the JAR currently contains invalid classes, don't actually use it// for classloadingif (!validateJarFile(file)) return;JarFile[] result2 = new JarFile[jarFiles.length + 1];for (i = 0; i < jarFiles.length; i++) {result2[i] = jarFiles[i];}result2[jarFiles.length] = jarFile;jarFiles = result2;// Add the file to the listFile[] result4 = new File[jarRealFiles.length + 1];for (i = 0; i < jarRealFiles.length; i++) {result4[i] = jarRealFiles[i];}result4[jarRealFiles.length] = file;jarRealFiles = result4;// Load manifestManifest manifest = jarFile.getManifest();if (manifest != null) {Iterator extensions = Extension.getAvailable(manifest).iterator();while (extensions.hasNext()) {available.add(extensions.next());}extensions = Extension.getRequired(manifest).iterator();while (extensions.hasNext()) {required.add(extensions.next());}}}/*** 是否有類文件或jar包被修改了?*/public boolean modified() {// Checking for modified loaded resourcesint length = paths.length;// A rare race condition can occur in the updates of the two arrays// It's totally ok if the latest class added is not checked (it will// be checked the next timeint length2 = lastModifiedDates.length;if (length > length2) length = length2;for (int i = 0; i < length; i++) {try {long lastModified = ((ResourceAttributes) resources.getAttributes(paths[i])).getLastModified();if (lastModified != lastModifiedDates[i]) {log("  Resource '" + paths[i] + "' was modified; Date is now: " + new java.util.Date(lastModified) + " Was: " + new java.util.Date(lastModifiedDates[i]));return (true);}} catch (NamingException e) {log("    Resource '" + paths[i] + "' is missing");return (true);}}length = jarNames.length;// Check if JARs have been added or removedif (getJarPath() != null) {try {NamingEnumeration myEnum = resources.listBindings(getJarPath());int i = 0;while (myEnum.hasMoreElements() && (i < length)) {NameClassPair ncPair = (NameClassPair) myEnum.nextElement();String name = ncPair.getName();// Ignore non JARs present in the lib folderif (!name.endsWith(".jar")) continue;if (!name.equals(jarNames[i])) {// Missing JARlog("    Additional JARs have been added : '" + name + "'");return (true);}i++;}if (myEnum.hasMoreElements()) {while (myEnum.hasMoreElements()) {NameClassPair ncPair = (NameClassPair) myEnum.nextElement();String name = ncPair.getName();// Additional non-JAR files are allowedif (name.endsWith(".jar")) {// There was more JARslog("    Additional JARs have been added");return (true);}}} else if (i < jarNames.length) {// There was less JARslog("    Additional JARs have been added");return (true);}} catch (NamingException e) {if (debug > 2) log("    Failed tracking modifications of '" + getJarPath() + "'");} catch (ClassCastException e) {log("    Failed tracking modifications of '" + getJarPath() + "' : " + e.getMessage());}}// No classes have been modifiedreturn (false);}}

StandardContext類

StandardContext類的具體內容將放到第十二章來講,這里僅列出它的 reload() 方法,即類重載的方法。看上去其實就是一個Context容器的重啟過程:先將容器實例及其相關組件實例stop掉,然后在start起來。

public synchronized void reload() {// Validate our current component stateif (!started) throw new IllegalStateException(sm.getString("containerBase.notStarted", logName()));// Make sure reloading is enabled//      if (!reloadable)//          throw new IllegalStateException//              (sm.getString("standardContext.notReloadable"));log(sm.getString("standardContext.reloadingStarted"));// Stop accepting requests temporarilysetPaused(true);// Binding threadClassLoader oldCCL = bindThread();// Shut down our session managerif ((manager != null) && (manager instanceof Lifecycle)) {try {((Lifecycle) manager).stop();} catch (LifecycleException e) {log(sm.getString("standardContext.stoppingManager"), e);}}// Shut down the current version of all active servletsContainer children[] = findChildren();for (int i = 0; i < children.length; i++) {Wrapper wrapper = (Wrapper) children[i];if (wrapper instanceof Lifecycle) {try {((Lifecycle) wrapper).stop();} catch (LifecycleException e) {log(sm.getString("standardContext.stoppingWrapper", wrapper.getName()), e);}}}// Shut down application event listenerslistenerStop();// Clear all application-originated servlet context attributesif (context != null) context.clearAttributes();// Shut down filtersfilterStop();if (isUseNaming()) {// StartnamingContextListener.lifecycleEvent(new LifecycleEvent(this, Lifecycle.STOP_EVENT));}// Binding threadunbindThread(oldCCL);// Shut down our application class loaderif ((loader != null) && (loader instanceof Lifecycle)) {try {((Lifecycle) loader).stop();} catch (LifecycleException e) {log(sm.getString("standardContext.stoppingLoader"), e);}}// Binding threadoldCCL = bindThread();// Restart our application class loaderif ((loader != null) && (loader instanceof Lifecycle)) {try {((Lifecycle) loader).start();} catch (LifecycleException e) {log(sm.getString("standardContext.startingLoader"), e);}}// Binding threadunbindThread(oldCCL);// Create and register the associated naming context, if internal// naming is usedboolean ok = true;if (isUseNaming()) {// StartnamingContextListener.lifecycleEvent(new LifecycleEvent(this, Lifecycle.START_EVENT));}// Binding threadoldCCL = bindThread();// Restart our application event listeners and filtersif (ok) {if (!listenerStart()) {log(sm.getString("standardContext.listenerStartFailed"));ok = false;}}if (ok) {if (!filterStart()) {log(sm.getString("standardContext.filterStartFailed"));ok = false;}}// Restore the "Welcome Files" and "Resources" context attributespostResources();postWelcomeFiles();// Restart our currently defined servletsfor (int i = 0; i < children.length; i++) {if (!ok) break;Wrapper wrapper = (Wrapper) children[i];if (wrapper instanceof Lifecycle) {try {((Lifecycle) wrapper).start();} catch (LifecycleException e) {log(sm.getString("standardContext.startingWrapper", wrapper.getName()), e);ok = false;}}}// Reinitialize all load on startup servletsloadOnStartup(children);// Restart our session manager (AFTER naming context recreated/bound)if ((manager != null) && (manager instanceof Lifecycle)) {try {((Lifecycle) manager).start();} catch (LifecycleException e) {log(sm.getString("standardContext.startingManager"), e);}}// Unbinding threadunbindThread(oldCCL);// Start accepting requests againif (ok) {log(sm.getString("standardContext.reloadingCompleted"));} else {setAvailable(false);log(sm.getString("standardContext.reloadingFailed"));}setPaused(false);// Notify our interested LifecycleListenerslifecycle.fireLifecycleEvent(Context.RELOAD_EVENT, null);}

Web應用程序

下面來構建兩個應用程序,作為Tomcat中存放的應用程序,他們分別由不同的載入器實例來加載。我們仍然使用前面章節用到的ModernServlet與PrimitiveServlet來作為應用程序的servlet。至于lib包下的simple-project-1.0-SNAPSHOT.jar則是只提供了一個MyObject類

?

MyObject類中打印了加載該類的類加載器,并提供了一個print方法

public class MyObject {public MyObject() {ClassLoader classLoader = this.getClass().getClassLoader();System.out.println("=======MyObject's classLoader is: "+classLoader.toString());}public void print() {System.out.println("=======MyObject print [AAAAAA]");}}

ModernServlet的doGet方法末尾使用了MyObject類,創建了一個MyObject對象并調用其print方法。

public class ModernServlet extends HttpServlet {public void init(ServletConfig config) {System.out.println("ModernServlet -- init");}public void doGet(HttpServletRequest request,HttpServletResponse response)throws ServletException, IOException {response.setContentType("text/html");PrintWriter out = response.getWriter();//先輸出HTTP的頭部信息String msg = "HTTP/1.1 200 OK\r\n" +"Content-Type: text/html\r\n" +"Transfer-Encoding: chunked\r\n" +"\r\n";out.print(msg);StringBuilder builder = new StringBuilder();//再輸出HTTP的消息體builder.append("<html>");builder.append("<head>");builder.append("<title>Modern Servlet</title>");builder.append("</head>");builder.append("<body>");builder.append("<h2>Headers</h2>");Enumeration headers = request.getHeaderNames();while (headers.hasMoreElements()) {String header = (String) headers.nextElement();builder.append("<br>" + header + " : " + request.getHeader(header));}builder.append("<br><h2>Method</h2>");builder.append("<br>" + request.getMethod());builder.append("<br><h2>Parameters</h2>");Enumeration parameters = request.getParameterNames();while (parameters.hasMoreElements()) {String parameter = (String) parameters.nextElement();builder.append("<br>" + parameter + " : " + request.getParameter(parameter));}builder.append("<br><h2>Query String</h2>");builder.append("<br>" + request.getQueryString());builder.append("<br><h2>Request URI</h2>");builder.append("<br>" + request.getRequestURI());builder.append("</body>");builder.append("</html>");// 這里是與原書中代碼不一樣的地方,原代碼沒有加chunked塊的長度,瀏覽器不能正常解析out.print(Integer.toHexString(builder.length()) + "\r\n");out.print(builder.toString() + "\r\n");out.print("0\r\n\r\n");out.flush();out.close();MyObject myObject = new MyObject();myObject.print();}
}

這里我加入MyObject這個類,是有兩種用途

  1. 檢測WebappClassLoader是否能加載 WEB-INF/lib 包下的jar包中的類。
  2. 在ModernServlet執行doGet方法時,發現其依賴MyObject,測試MyObject類是否是和ModernServlet用的同一個類加載實例來加載的。

另外我構建了兩個應用程序 myApp與myApp2,使用了同一套代碼,我們可以來檢查下,這兩個應用程序下相同的類是否是由不同的WebappClassLoader實例來加載的。

我寫了一段檢測不同類的類加載器的代碼,放在了Wrapper容器的基礎閥SimpleWrapperValve中,在Wrapper實例獲取到對應的servlet后,打印了一下加載該servlet的類加載器;這里還打印了一下HttpServlet類的類加載器,HttpServlet類在項目最外層的lib包中,是我設置的整個項目的lib依賴,這個lib包理應會被加入到應用程序類加載器的類庫中,所以HttpServlet類的類加載器應該是?sun.misc.Launcher.AppClassLoader

public class SimpleWrapperValve implements Valve, Contained {protected Container container;public void invoke(Request request, Response response, ValveContext valveContext)throws IOException, ServletException {SimpleWrapper wrapper = (SimpleWrapper) getContainer();ServletRequest sreq = request.getRequest();ServletResponse sres = response.getResponse();Servlet servlet = null;HttpServletRequest hreq = null;if (sreq instanceof HttpServletRequest) {hreq = (HttpServletRequest) sreq;}HttpServletResponse hres = null;if (sres instanceof HttpServletResponse) {hres = (HttpServletResponse) sres;}// 分配一個servlet實例來處理請求try {servlet = wrapper.allocate();if (hres != null && hreq != null) {System.out.println("servlet's classLoader is " + servlet.getClass().getClassLoader().toString());System.out.println("HttpServlet's classLoader is " + HttpServlet.class.getClassLoader().toString());servlet.service(hreq, hres);} else {servlet.service(sreq, sres);}} catch (ServletException e) {}}public String getInfo() {return null;}public Container getContainer() {return container;}public void setContainer(Container container) {this.container = container;}
}

wrapper.allocate()這個方法是調用的SimpleWrapper的allocate方法,allocate會調用loadServlet方法來進行servlet類的類加載,我把代碼放這,你可以再看下

public class SimpleWrapper implements Wrapper, Pipeline, Lifecycle {public SimpleWrapper() {pipeline.setBasic(new SimpleWrapperValve());}// the servlet instanceprivate Servlet instance = null;private String servletClass;private Loader loader;private String name;protected LifecycleSupport lifecycle = new LifecycleSupport(this);private SimplePipeline pipeline = new SimplePipeline(this);protected Container parent = null;protected boolean started = false;public synchronized void addValve(Valve valve) {pipeline.addValve(valve);}public Servlet allocate() throws ServletException {// Load and initialize our instance if necessaryif (instance == null) {try {instance = loadServlet();} catch (ServletException e) {throw e;} catch (Throwable e) {throw new ServletException("Cannot allocate a servlet instance", e);}}return instance;}public Servlet loadServlet() throws ServletException {if (instance != null) return instance;Servlet servlet = null;String actualClass = servletClass;if (actualClass == null) {throw new ServletException("servlet class has not been specified");}Loader loader = getLoader();// Acquire an instance of the class loader to be usedif (loader == null) {throw new ServletException("No loader.");}ClassLoader classLoader = loader.getClassLoader();// Load the specified servlet class from the appropriate class loaderClass classClass = null;try {if (classLoader != null) {classClass = classLoader.loadClass(actualClass);}} catch (ClassNotFoundException e) {throw new ServletException("Servlet class not found");}// Instantiate and initialize an instance of the servlet class itselftry {servlet = (Servlet) classClass.newInstance();} catch (Throwable e) {throw new ServletException("Failed to instantiate servlet");}// Call the initialization method of this servlettry {servlet.init(null);} catch (Throwable f) {throw new ServletException("Failed initialize servlet.");}return servlet;}public Loader getLoader() {if (loader != null) return (loader);if (parent != null) return (parent.getLoader());return (null);}}

SimpleWrapper的getLoader方法會主動去尋找父容器的Loader,所以本次程序我們只要給Context容器設置好Loader就行(WebappLoader)。

通過loader就能獲取到對應的類加載器,也就是WebappClassLoader的實例,然后通過WebappClassLoader去加載該servlet類。

如果瀏覽器訪問?http://localhost:8080/myApp/Modern,WebappClassLoader實例先加載了 ModernServlet 類,然后ModernServlet的doGet方法被調用,發現需要使用MyObject類,于是JVM拿到ModernServlet的類加載器(即WebappClassLoader實例)去加載MyObject類。

接下來我們寫個啟動類,啟動一個簡易Tomcat,來驗證上述說法

Bootstrap啟動類

本次context容器類使用Tomcat內置的StandardContext,為了驗證兩個應用的類加載隔離性,這次還將用上StandardHost作為Host容器,Host容器的內容將在第十三章介紹。

我將構建兩個一模一樣的StandardContext實例,唯一不同是就是它們加載的應用程序目錄不同,一個是myApp,一個是myApp2。理論上來講,兩個應用程序內的類應該由不同的類加載器來加載,接下來啟動服務來驗證看看

package ex08.pyrmont.startup;import ex08.pyrmont.core.SimpleWrapper;
import ex08.pyrmont.core.SimpleContextConfig;
import org.apache.catalina.*;
import org.apache.catalina.connector.http.HttpConnector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.loader.WebappClassLoader;
import org.apache.catalina.loader.WebappLoader;
import org.apache.naming.resources.ProxyDirContext;public final class Bootstrap {public static void main(String[] args) {System.setProperty("catalina.base", System.getProperty("user.dir"));Connector connector = new HttpConnector();Host host = new StandardHost();host.setName("localhost");host.setAppBase("");{Wrapper wrapper1 = new SimpleWrapper();wrapper1.setName("Primitive");wrapper1.setServletClass("PrimitiveServlet");Wrapper wrapper2 = new SimpleWrapper();wrapper2.setName("Modern");wrapper2.setServletClass("ModernServlet");Context context = new StandardContext();// StandardContext's start method adds a default mappercontext.setPath("/myApp");context.setDocBase("myApp");context.addChild(wrapper1);context.addChild(wrapper2);// context.addServletMapping(pattern, name);context.addServletMapping("/Primitive", "Primitive");context.addServletMapping("/Modern", "Modern");// add ContextConfig. This listener is important because it configures// StandardContext (sets configured to true), otherwise StandardContext// won't startLifecycleListener listener = new SimpleContextConfig();((Lifecycle) context).addLifecycleListener(listener);// here is our loaderLoader loader = new WebappLoader();// associate the loader with the Contextcontext.setLoader(loader);host.addChild(context);}{Wrapper wrapper1 = new SimpleWrapper();wrapper1.setName("Primitive");wrapper1.setServletClass("PrimitiveServlet");Wrapper wrapper2 = new SimpleWrapper();wrapper2.setName("Modern");wrapper2.setServletClass("ModernServlet");Context context = new StandardContext();// StandardContext's start method adds a default mappercontext.setPath("/myApp2");context.setDocBase("myApp2");context.addChild(wrapper1);context.addChild(wrapper2);// context.addServletMapping(pattern, name);context.addServletMapping("/Primitive", "Primitive");context.addServletMapping("/Modern", "Modern");// add ContextConfig. This listener is important because it configures// StandardContext (sets configured to true), otherwise StandardContext// won't startLifecycleListener listener = new SimpleContextConfig();((Lifecycle) context).addLifecycleListener(listener);// here is our loaderLoader loader = new WebappLoader();// associate the loader with the Contextcontext.setLoader(loader);host.addChild(context);}connector.setContainer(host);try {connector.initialize();((Lifecycle) connector).start();((Lifecycle) host).start();// make the application wait until we press a key.System.in.read();((Lifecycle) host).stop();}catch (Exception e) {e.printStackTrace();}}
}

啟動服務,瀏覽器訪問兩下

1. http://localhost:8080/myApp/Modern
2. http://localhost:8080/myApp2/Modern

瀏覽器的結果與前幾章的一樣,不再貼了,主要看后端日志

Connected to the target VM, address: '127.0.0.1:64327', transport: 'socket'
HttpConnector Opening server socket on all host IP addresses
HttpConnector[8080] Starting background thread
WebappLoader[/myApp2]: Deploying class repositories to work directory /Users/hml/IdeaProjects/demo/HowTomcatWorks/work/_/localhost/myApp2
WebappLoader[/myApp2]: Deploy class files /WEB-INF/classes to /Users/hml/IdeaProjects/demo/HowTomcatWorks/myApp2/WEB-INF/classes
WebappLoader[/myApp2]: Deploy JAR /WEB-INF/lib/simple-project-1.0-SNAPSHOT.jar to /Users/hml/IdeaProjects/demo/HowTomcatWorks/myApp2/WEB-INF/lib/simple-project-1.0-SNAPSHOT.jar
Starting Wrapper Primitive
Starting Wrapper Modern
StandardManager[/myApp2]: Seeding random number generator class java.security.SecureRandom
StandardManager[/myApp2]: Seeding of random number generator has been completed
WebappLoader[/myApp]: Deploying class repositories to work directory /Users/hml/IdeaProjects/demo/HowTomcatWorks/work/_/localhost/myApp
WebappLoader[/myApp]: Deploy class files /WEB-INF/classes to /Users/hml/IdeaProjects/demo/HowTomcatWorks/myApp/WEB-INF/classes
WebappLoader[/myApp]: Deploy JAR /WEB-INF/lib/simple-project-1.0-SNAPSHOT.jar to /Users/hml/IdeaProjects/demo/HowTomcatWorks/myApp/WEB-INF/lib/simple-project-1.0-SNAPSHOT.jar
Starting Wrapper Primitive
Starting Wrapper Modern
StandardManager[/myApp]: Seeding random number generator class java.security.SecureRandom
StandardManager[/myApp]: Seeding of random number generator has been completed
ModernServlet -- init
=======servlet's classLoader is org.apache.catalina.loader.WebappClassLoader@4bf558aa
=======HttpServlet's classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
=======MyObject's classLoader is: org.apache.catalina.loader.WebappClassLoader@4bf558aa
=======MyObject print [AAAAAA]
ModernServlet -- init
=======servlet's classLoader is org.apache.catalina.loader.WebappClassLoader@7cca494b
=======HttpServlet's classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
=======MyObject's classLoader is: org.apache.catalina.loader.WebappClassLoader@7cca494b
=======MyObject print [AAAAAA]

可以看到 HttpServlet的類加載是應用程序類加載器?AppClassLoader ,并且myApp與myApp2兩個應用的AppClassLoader是同一個對象。說明Tomcat中的應用程序類加載器是唯一的,多應用公用的。

而加載ModernServlet與MyObject兩個類的類加載為WebappClassLoader,并且myApp與myApp2兩個應用的WebappClassLoader是不同的對象,說明兩個應用的類加載是隔離的。

MyObject類與ModernServlet類使用的類加載相同,也證實了上面的說法,當JVM檢測到某類依賴的另外的類需要還沒加載時,會拿當前類的類加載器去加載。

好,Tomcat的載入器內容就到這里,Tomcat的自定義類加載器是一個經典的打破雙親委派機制的案例,目的是為了實現各應用程序之間的類庫隔離。

另外,關于 common、server、shared三個類加載器并不像 WebApp類加載器一樣有特定的類(WebappClassLoader)來支撐。Tomcat中并沒有類似的諸如CommonClassLoader、ServerClassLoader等類,而是提供了StandardClassLoader類,這三個類加載器都是一個StandardClassLoader類實例,不同的是,他們可加載的類庫不同,這個類庫就定義在?catalina.properties 文件中,這三個類加載的創建邏輯在org.apache.catalina.startup.Bootstrap#initClassLoaders方法中(基于apache-tomcat-6.09版本)

org.apache.catalina.startup.Bootstrap部分代碼

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) {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;ArrayList repositoryLocations = new ArrayList();ArrayList repositoryTypes = new ArrayList();int i;StringTokenizer tokenizer = new StringTokenizer(value, ",");while (tokenizer.hasMoreElements()) {String repository = tokenizer.nextToken();// Local repositoryboolean replace = false;String before = repository;while ((i=repository.indexOf(CATALINA_HOME_TOKEN))>=0) {replace=true;if (i>0) {repository = repository.substring(0,i) + getCatalinaHome() + repository.substring(i+CATALINA_HOME_TOKEN.length());} else {repository = getCatalinaHome() + repository.substring(CATALINA_HOME_TOKEN.length());}}while ((i=repository.indexOf(CATALINA_BASE_TOKEN))>=0) {replace=true;if (i>0) {repository = repository.substring(0,i) + getCatalinaBase() + repository.substring(i+CATALINA_BASE_TOKEN.length());} else {repository = getCatalinaBase() + repository.substring(CATALINA_BASE_TOKEN.length());}}if (replace && log.isDebugEnabled())log.debug("Expanded " + before + " to " + replace);// Check for a JAR URL repositorytry {URL url=new URL(repository);repositoryLocations.add(repository);repositoryTypes.add(ClassLoaderFactory.IS_URL);continue;} catch (MalformedURLException e) {// Ignore}if (repository.endsWith("*.jar")) {repository = repository.substring(0, repository.length() - "*.jar".length());repositoryLocations.add(repository);repositoryTypes.add(ClassLoaderFactory.IS_GLOB);} else if (repository.endsWith(".jar")) {repositoryLocations.add(repository);repositoryTypes.add(ClassLoaderFactory.IS_JAR);} else {repositoryLocations.add(repository);repositoryTypes.add(ClassLoaderFactory.IS_DIR);}}String[] locations = (String[]) repositoryLocations.toArray(new String[0]);Integer[] types = (Integer[]) repositoryTypes.toArray(new Integer[0]);ClassLoader classLoader = ClassLoaderFactory.createClassLoader(locations, types, parent);// Retrieving MBean serverMBeanServer mBeanServer = null;if (MBeanServerFactory.findMBeanServer(null).size() > 0) {mBeanServer =(MBeanServer) MBeanServerFactory.findMBeanServer(null).get(0);} else {mBeanServer = MBeanServerFactory.createMBeanServer();}// Register the server classloaderObjectName objectName =new ObjectName("Catalina:type=ServerClassLoader,name=" + name);mBeanServer.registerMBean(classLoader, objectName);return classLoader;}

org.apache.catalina.startup.ClassLoaderFactory#createClassLoader方法

public static ClassLoader createClassLoader(String locations[],Integer types[],ClassLoader parent)throws Exception {if (log.isDebugEnabled())log.debug("Creating new class loader");// Construct the "class path" for this class loaderArrayList list = new ArrayList();if (locations != null && types != null && locations.length == types.length) {for (int i = 0; i < locations.length; i++)  {String location = locations[i];if ( types[i] == IS_URL ) {URL url = new URL(location);if (log.isDebugEnabled())log.debug("  Including URL " + url);list.add(url);} else if ( types[i] == IS_DIR ) {File directory = new File(location);directory = new File(directory.getCanonicalPath());if (!directory.exists() || !directory.isDirectory() ||!directory.canRead())continue;URL url = directory.toURL();if (log.isDebugEnabled())log.debug("  Including directory " + url);list.add(url);} else if ( types[i] == IS_JAR ) {File file=new File(location);file = new File(file.getCanonicalPath());if (!file.exists() || !file.canRead())continue;URL url = file.toURL();if (log.isDebugEnabled())log.debug("  Including jar file " + url);list.add(url);} else if ( types[i] == IS_GLOB ) {File directory=new File(location);if (!directory.exists() || !directory.isDirectory() ||!directory.canRead())continue;if (log.isDebugEnabled())log.debug("  Including directory glob "+ directory.getAbsolutePath());String filenames[] = directory.list();for (int j = 0; j < filenames.length; j++) {String filename = filenames[j].toLowerCase();if (!filename.endsWith(".jar"))continue;File file = new File(directory, filenames[j]);file = new File(file.getCanonicalPath());if (!file.exists() || !file.canRead())continue;if (log.isDebugEnabled())log.debug("    Including glob jar file "+ file.getAbsolutePath());URL url = file.toURL();list.add(url);}}}}// Construct the class loader itselfURL[] array = (URL[]) list.toArray(new URL[list.size()]);if (log.isDebugEnabled())for (int i = 0; i < array.length; i++) {log.debug("  location " + i + " is " + array[i]);}StandardClassLoader classLoader = null;if (parent == null)classLoader = new StandardClassLoader(array);elseclassLoader = new StandardClassLoader(array, parent);return (classLoader);}

關于Tomcat與SpringBoot

如今我們在用java寫Web應用程序時多使用SpringBoot框架,SpringBoot默認集成了Tomcat,這也導致了一個Tomcat中只會部署一個應用程序,自定義類加載器的存在感就大大降低了。

我寫了個測試類來測試了一下

輸出結果

null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2

redisTemplate就是我們熟知的那個RedisTemplate,luaService是我自己寫的標注了@Service的類,HObject就是一個普普通通不帶任何注解的pojo類。

實驗結果就是除了String被啟動類加載器加載了,其他的類都是被jdk自帶的應用程序類加載器加載的,并且我在這個springboot自帶的tomcat的 WebappClassLoaderBase#loadClass 方法上打了斷點,發現項目啟動后壓根沒有進入到過這個方法中,也就是WebappClassLoader壓根沒用上。

springboot的東西再深挖又得燒腦了,容我休息一下,這里暫且告一段落,后續再做研究。

源碼分享

https://gitee.com/huo-ming-lu/HowTomcatWorks

基于原書源碼,我改造了Bootstrap以支持啟動一個多應用的簡易Tomcat。

我改造了ModernServlet類并加入simple-project-1.0-SNAPSHOT.jar包來驗證WebApp應用程序中除servlet類外的其他類的類加載過程。并復制了myApp工程得到myApp2工程,來驗證多應用間的類加載隔離。

??

另附Tomcat各版本源碼的下載目錄

https://archive.apache.org/dist/tomcat/

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/13109.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/13109.shtml
英文地址,請注明出處:http://en.pswp.cn/web/13109.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

環形數組介紹要點和難點具體應用實例和代碼解析

環形數組(或稱為循環數組、圓形數組)是一種邏輯結構,其中數組的末尾和開頭在邏輯上是相連的,從而形成一個環或圈。在實際的物理存儲中,環形數組通常是一個普通的線性數組,但在訪問和操作時采用特定的邏輯來處理邊界條件,使得元素可以從數組的末尾“循環”到開頭,或者從…

基于 Spring Boot 博客系統開發(十)

基于 Spring Boot 博客系統開發&#xff08;十&#xff09; 本系統是簡易的個人博客系統開發&#xff0c;為了更加熟練地掌握 SprIng Boot 框架及相關技術的使用。&#x1f33f;&#x1f33f;&#x1f33f; 基于 Spring Boot 博客系統開發&#xff08;九&#xff09;&#x1f…

MySQL 開源到商業(四):MySQL 成了燙手山芋

前文提到&#xff0c;Monty 得知 Oracle 收購 Sun 的提案得到了美國政府的支持后&#xff0c;發動社區用戶向歐盟委員會請愿&#xff0c;希望通過反壟斷的名義讓 Oracle 知難而退&#xff0c;進而實現剝離 MySQL 的目的。而 Oracle 為了得到歐盟委員會的許可&#xff0c;迅速提…

Golang | Leetcode Golang題解之第91題解碼方法

題目&#xff1a; 題解&#xff1a; func numDecodings(s string) int {n : len(s)// a f[i-2], b f[i-1], c f[i]a, b, c : 0, 1, 0for i : 1; i < n; i {c 0if s[i-1] ! 0 {c b}if i > 1 && s[i-2] ! 0 && ((s[i-2]-0)*10(s[i-1]-0) < 26) {c…

Navicat 干貨 | 探索 PostgreSQL 中不同類型的約束

PostgreSQL 的一個重要特性之一是能夠對數據實施各種約束&#xff0c;以確保數據完整性和可靠性。今天的文章中&#xff0c;我們將概述 PostgreSQL 的各種約束類型并結合免費的 "dvdrental" 示例數據庫 中的例子探索他們的使用方法。 1. 檢查約束&#xff1a; 檢查…

顏色的表示和還原(一)

這篇文章主要提煉于ICCV 2019 Tutorial: Understanding Color and the In-Camera Image Processing Pipeline for Computer Vision。里面深入淺出地講解了很多ISP中的基礎知識&#xff0c;這里主要對顏色相關的部分做一點總結。 假設不成立了 相機經常被簡單地看作是衡量光線…

STM32學習計劃

前言&#xff1a; 這里先記錄下STM32的學習計劃。 2024/05/08 今天我正在學習的是正點原子的I.MX6ULL APLHA/Mini 開發板的 Linux 之ARM裸機第二期開發的視頻教程&#xff0c;會用正點原子的I.MX6ULL開發板學習第二期ARM裸機開發的教程&#xff0c;然后是學習完正點原子的I.M…

Mybatis基礎操作-刪除

Mybatis基礎操作-刪除 刪除 package com.itheima.mapper;import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Mapper;Mapper //在運行時&#xff0c;會自動生成該接口的實現類對象&#xff08;代理對象&#xff09;&#xff0c;并且將該對象…

QT:QML與C++交互

目錄 一.介紹 二.pro文件添加模塊 三.h文件 四.cpp文件 五.注冊 六.調用 七.展示效果 八.代碼 1.qmlandc.h 2.qmlandc.cpp 3.main.cpp 4.qml 一.介紹 在 Qt 中&#xff0c;QML 與 C 交互是非常重要的&#xff0c;因為它允許開發人員充分利用 QML 和 C 各自的優勢&…

我21歲玩“擼貨”,被騙1000多萬

最近&#xff0c;擼貨業界內發生了一些頗受矚目的事件。 在鄭州&#xff0c;數碼檔口下面搶手團長跑路失聯&#xff0c;涉及金額幾百萬&#xff0c;在南京&#xff0c;一家知名的電商平臺下的收貨站點突然失聯&#xff0c;涉及金額高達一千多萬&#xff0c;令眾多交易者震驚不已…

用scp將文件夾從一個服務器備份到另一個服務器

用scp將文件夾從一個服務器備份到另一個服務器 問題描述解決辦法 問題描述 公式服務器要回收了&#xff0c;如何將數據備份到另一個服務器上。 解決辦法 代碼如下 scp -P 32660 -r /path/of/the/original/file username10.258.36.187:/path/of/the/target/filescp -P 目標…

YOLOv8改進 | 圖像修復 | 適用多種復雜場景的全能圖像修復網絡AirNet助力YOLOv8檢測(全網獨家首發)

一、本文介紹 本文給大家帶來的改進機制是一種適用多種復雜場景的全能圖像修復網絡AirNet&#xff0c;其由對比基降解編碼器&#xff08;CBDE&#xff09;和降解引導修復網絡&#xff08;DGRN&#xff09;兩個神經模塊組成&#xff0c;能夠在未知損壞類型和程度的情況下恢復受…

Java | Leetcode Java題解之第92題反轉鏈表II

題目&#xff1a; 題解&#xff1a; class Solution {public ListNode reverseBetween(ListNode head, int left, int right) {// 設置 dummyNode 是這一類問題的一般做法ListNode dummyNode new ListNode(-1);dummyNode.next head;ListNode pre dummyNode;for (int i 0; …

【SQL】SQL常見面試題總結(3)

目錄 1、聚合函數1.1、SQL 類別高難度試卷得分的截斷平均值&#xff08;較難&#xff09;1.2、統計作答次數1.3、得分不小于平均分的最低分 2、分組查詢2.1、平均活躍天數和月活人數2.2、月總刷題數和日均刷題數2.3、未完成試卷數大于 1 的有效用戶&#xff08;較難&#xff09…

藍橋杯 EDA 組 歷屆國賽真題解析

一、2021年國賽真題 1.1 CN3767 太陽能充電電路 CN3767 是具有太陽能電池最大功率點跟蹤功能的 4A&#xff0c;12V 鉛酸電池充電管理集成電路。 最大功率點應指的是電池板的輸出電壓&#xff0c;跟蹤電壓其做保護。當然 CN3767 也可以直接使用直流充電&#xff0c;具體可以閱讀…

ROS 2邊學邊練(49)-- 生成URDF文件

前言 大多數機器人學家都在團隊中工作&#xff0c;這些團隊中往往包括機械工程師&#xff0c;他們負責開發機器人的CAD模型。與手動創建URDF&#xff08;統一機器人描述格式&#xff09;文件不同&#xff0c;可以從許多不同的CAD和建模程序中導出URDF模型。這些導出工具通常…

[POJ-1321]棋盤問題

題源:POJ-1321 深搜板子題&#xff0c;非常基礎&#xff0c;難度不大 思路1&#xff1a;廣搜行 深搜列 #include<iostream> #include<cstring> using namespace std; const int MAX9; int a,b,ans; char m[MAX][MAX]; //深搜列&#xff0c;廣搜行 bool h[MAX]; v…

DS高階:跳表

一、skiplist 1.1 skiplist的概念 skiplist本質上也是一種查找結構&#xff0c;用于解決算法中的查找問題&#xff0c;跟平衡搜索樹和哈希表的價值是一樣的&#xff0c;可以作為key或者key/value的查找模型。skiplist是由William Pugh發明的&#xff0c;最早出現于他在1990年發…

Python學習之路 | Python基礎語法(一)

數據類型 Python3 中常見的數據類型有&#xff1a; Number&#xff08;數字&#xff09;String&#xff08;字符串&#xff09;bool&#xff08;布爾類型&#xff09;List&#xff08;列表&#xff09;Tuple&#xff08;元組&#xff09;Set&#xff08;集合&#xff09;Dict…

鴻蒙HDC命令行工具:模擬操作

模擬操作 uinput用于輸入模擬操作&#xff0c;其命令幫助手冊為&#xff1a; > hdc shell uinput --help Usage: uinput <option> <command> <arg>... The option are: -M --mouse //模擬鼠標操作 commands for mouse: -m <dx> <d…