Tomcat啟動時classloader加載順序
Tomcat的class加載的優先順序一覽
1.最先是$JAVA_HOME/jre/lib/ext/下的jar文件。
2.環境變量CLASSPATH中的jar和class文件。
3.$CATALINA_HOME/common/classes下的class文件。
4.$CATALINA_HOME/commons/endorsed下的jar文件。
5.$CATALINA_HOME/commons/i18n下的jar文件。
6.$CATALINA_HOME/common/lib 下的jar文件。
(JDBC驅動之類的jar文件可以放在這里,這樣就可以避免在server.xml配置好數據源卻出現找不到JDBC Driver的情況。)
7.$CATALINA_HOME/server/classes下的class文件。
8.$CATALINA_HOME/server/lib/下的jar文件。
9.$CATALINA_BASE/shared/classes 下的class文件。
10.$CATALINA_BASE/shared/lib下的jar文件。
11.各自具體的webapp /WEB-INF/classes下的class文件。
12.各自具體的webapp /WEB-INF/lib下的jar文件。
class的搜尋順序如下:
-------------
Bootstrap classes of your JVM
System class loader classses (described above)
/WEB-INF/classes of your web application
/WEB-INF/lib/*.jar of your web application
$CATALINA_HOME/common/classes
$CATALINA_HOME/common/endorsed/*.jar
$CATALINA_HOME/common/i18n/*.jar
$CATALINA_HOME/common/lib/*.jar
$CATALINA_BASE/shared/classes
$CATALINA_BASE/shared/lib/*.jar
--------------
因此放在不同webapp里的class文件,會被classloader加載成不同的實例。
例如假設下面兩個不同內容的class。分別放在不同的webapp的class目錄下。
package com.lizongbo;
public class TestClass {
private String NAME="lizongbo";
}
package com.lizongbo;
public class TestClass {
private String NAME="li_zongbo";
}
在不同的webapp得到的com.lizongbo.NAME結果是不同的,且互不影響。
但是注意,以下包名開頭的class例外:
javax.*
org.xml.sax.*
org.w3c.dom.*
org.apache.xerces.*
org.apache.xalan.*
ps,注意.在各個jar中的\META-INF\MAINFEST.MF文件里Class-Path鍵值對,也會提供jar的加載優先順序。
例如某jar的MAINFEST.MF內容如下:
Manifest-Version: 1.0
Created-By: lizongbo
Class-Path: commons-beanutils.jar
Class-Path: commons-collections.jar
Class-Path: commons-dbcp.jar
Class-Path: commons-digester.jar
Class-Path: commons-logging.jar
Class-Path: commons-pool.jar
Class-Path: commons-services.jar
Class-Path: commons-validator.jar
Class-Path: jakarta-oro.jar
Main-Class: com.lizongbo.MyTestClass
那么在加載這個jar的時候,會先在此jar所在目錄下依次先加載commons-beanutils.jar,commons-collections.jar。。。等jar文件。
在不同的地方放置jar和class可能會產生意想不到的后果,,尤其是不同版本的jar文件,因此在實際應用部署web應用時候要特別留心.
例如 使用javamail常見的一個出錯信息:
javax.mail.NoSuchProviderException: No provider for smtp
其真實原因就很可能如下:
在不同的加載jar的目錄下放置了不同版本的mail.jar,比如一個是javamail1.3.1的mail.jar
在D:\jakarta-tomcat-5.5.8\common\lib下,而另外一個是javamail1.3.2的mail.jar在
D:\jakarta-tomcat-5.5.8\webapps\lizongbo\WEB-INF/lib下,
那么lizongbo這個webapp中使用到javamail進行郵件發送的時候,便會出現No provider for smtp的錯誤。
*******************************************************************
ClassLoader in Tomcat
************************************
1 - Tomcat 的類載入器的結構
Tomcat Server 在啟動的時候將構造一個ClassLoader樹,以保證模塊的類庫是私有的
Tomcat Server的ClassLoader結構如下:
+-----------------------------+
| Bootstrap |
| | |
| System |
| | |
| Common |
| / \ |
| Catalina Shared |
| / \ |
| WebApp1 WebApp2 |
+-----------------------------+
其中:
- Bootstrap - 載入JVM自帶的類和$JAVA_HOME/jre/lib/ext/*.jar
- System - 載入$CLASSPATH/*.class
- Common - 載入$CATALINA_HOME/common/...,它們對TOMCAT和所有的WEB APP都可見
- Catalina - 載入$CATALINA_HOME/server/...,它們僅對TOMCAT可見,對所有的WEB APP都不可見
- Shared - 載入$CATALINA_HOME/shared/...,它們僅對所有WEB APP可見,對TOMCAT不可見(也不必見)
- WebApp - 載入ContextBase?/WEB-INF/...,它們僅對該WEB APP可見
2 - ClassLoader 的工作原理
每個運行中的線程都有一個成員contextClassLoader,用來在運行時動態地載入其它類
系統默認的contextClassLoader是systemClassLoader,所以一般而言java程序在執行時可以使用JVM自帶的類、$JAVA_HOME/jre/lib/ext/中的類和$CLASSPATH/中的類
可以使用 Thread.currentThread().setContextClassLoader(...); 更改當前線程的contextClassLoader,來改變其載入類的行為
ClassLoader 被組織成樹形,一般的工作原理是:
1) 線程需要用到某個類,于是contextClassLoader被請求來載入該類
2) contextClassLoader請求它的父ClassLoader來完成該載入請求
3) 如果父ClassLoader無法載入類,則contextClassLoader試圖自己來載入
注意 :WebApp?ClassLoader的工作原理和上述有少許不同:
它先試圖自己載入類(在ContextBase?/WEB-INF/...中載入類),如果無法載入,再請求父ClassLoader完成
由此可得:
- 對于WEB APP線程,它的contextClassLoader是WebApp?ClassLoader
- 對于Tomcat Server線程,它的contextClassLoader是CatalinaClassLoader
3 類的查找
ClassLoader類中loadClass方法為缺省實現,用下面的順序查找類:
1、調用findLoadedClass方法來檢查是否已經被加載。如果沒有則繼續下面的步驟。
2、如果當前類裝載器有一個指定的委托父裝載器,則用委托父裝載器的loadClass方法加載類,也就是委托給父裝載器加載相應的類。
3、如果這個類裝載器的委托層級體系沒有一個類裝載器加載該類,則使用類裝載器定位類的特定實現機制,調用findClass方法來查找類。
4 - 部分原代碼分析
4.1 - org/apache/catalina/startup/Bootstrap.java
Bootstrap中定義了三個classloader:commonLoader,catalinaLoader,sharedLoader.三者關系如下:
// 注意三個自己定置的ClassLoader的層次關系:
// systemClassLoader (root)
// +--- commonLoader
// +--- catalinaLoader
// +--- sharedLoader
Tomcat Server 線程的起點
構造ClassLoader樹,通過Thread.currentThread().setContextClassLoader(catalinaLoader)設置當前的classloader為catalinaLoader。
載入若干類,然后轉入org.apache.catalina.startup.Catalina類中
4.2 org.apache.catalina.loader.StandardClassLoader.java
通過看loadClass這個方法來看tomcat是如何加載類的,順序如下:
(0) Check our previously loaded class cache查找已經裝載的class
clazz = findLoadedClass(name);
(1) If a system class, use system class loader通過系統classloader來裝載class
ClassLoader loader = system;
clazz = loader.loadClass(name);
(2) Delegate to our parent if requested如果有代理則使用父類classloader
ClassLoader loader = parent;
if (loader == null)
loader = system;
clazz = loader.loadClass(name);
(3) Search local repositories 查找本地類池,比如$CATALINA_HOME/server
clazz = findClass(name);
(4) Delegate to parent unconditionally 默認使用代理裝載器
[ 查看代碼]
4.3 - org/apache/catalina/startup/ClassLoaderFactory.java
根據設置創建并返回StandardClassLoader的實例
[ 查看代碼]
4.4 - org/apache/catalina/loader/StandardClassLoader.java
類載入器
4.5 - org/apache/catalina/startup/SecurityClassLoad.java
該類僅包含一個靜態方法,用來為catalinaLoader載入一些類
[ 查看代碼]
Appendix - 參考
[1] http://jakarta.apache.org/tomcat/ 中的Tomcat 4.1.x文檔Class Loader HOW-TO
在一個 JVM 中可能存在多個 ClassLoader ,每個 ClassLoader 擁有自己的 NameSpace 。一個
ClassLoader 只能擁有一個 class 對象類型的實例,但是不同的 ClassLoader 可能擁有相同的 class
對象實例,這時可能產生致命的問題。如 ClassLoaderA ,裝載了類 A 的類型實例 A1 ,而 ClassLoaderB ,也裝載了類
A 的對象實例 A2 。邏輯上講 A1=A2 ,但是由于 A1 和 A2 來自于不同的 ClassLoader ,它們實際上是完全不同的,如果
A 中定義了一個靜態變量 c ,則 c 在不同的 ClassLoader 中的值是不同的。