Tomcat類加載
與很多服務(wù)器應(yīng)用一樣,Tomcat也實現(xiàn)了類加載器。
在Java虛擬機類加載的基礎(chǔ)上播瞳,Tomcat進行了稍許改動,以適應(yīng)自身業(yè)務(wù)的需求免糕。
當(dāng)Tomcat啟動后赢乓,它會創(chuàng)建一組類加載器忧侧,這些類加載器也會形成雙親委派模型中的父子關(guān)系,父類加載器在子類加載器之上:
Bootstrap
|
System
|
Common
/ \
Webapp1 Webapp2 ...
結(jié)合雙親委派模型來看牌芋,Tomcat的類加載結(jié)構(gòu)蚓炬,如圖所示:
Tomcat在原有雙親委派模型的基礎(chǔ)上,增加了Common類加載器躺屁、Catalina類加載器试吁、Shared類加載,以及WebApp類加載器楼咳。
如果將兩者結(jié)合起來看熄捍,那么Bootstrap對應(yīng)的是Bootstrap ClassLoader和Extension ClassLoader,System對應(yīng)的是Application ClassLoader母怜,Common對應(yīng)的是Common ClassLoader余耽、Catalina ClassLoader和Shared ClassLoader,WebApp1/WebApp2..對應(yīng)的是WebApp ClassLoader苹熏。
Bootstrap ClassLoader:C++編寫碟贾,無具體實現(xiàn)類,無法通過方法獲得轨域,負責(zé)加載<Java_Runtime_Home>/lib目錄下的文件袱耽。
Extension ClassLoader:Java程序編寫,sun.misc.Launcher.ExtClassLoader實現(xiàn)干发,負責(zé)加載<Java_Runtime_Home>/lib/ext目錄下的文件朱巨。
Application ClassLoader:Java語言編寫,sun.misc.Launcher.AppClassLoader實現(xiàn)枉长,負責(zé)加載Tomcat目錄下lib文件夾下的bootstrap.jar冀续、commons-daemon.jar、tomcat-juli.jar必峰。
Common ClassLoader:Java語言編寫洪唐,org.apache.catalina.loader.StandardClassLoader實現(xiàn)(在tomcat源碼中),負責(zé)加載Tomcat目錄conf文件夾下的catalina.properties文件中的common.loader屬性所指定的目錄吼蚁。
Catalina ClassLoader:Java語言編寫凭需,org.apache.catalina.loader.StandardClassLoader實現(xiàn)(在tomcat源碼中),負責(zé)加載Tomcat目錄conf文件夾下的catalina.properties文件中的server.loader屬性所指定的目錄肝匆。
Shared ClassLoader:Java語言編寫粒蜈,org.apache.catalina.loader.StandardClassLoader實現(xiàn)(在tomcat源碼中),負責(zé)加載Tomcat目錄conf文件夾下的catalina.properties文件中的shared.loader屬性所指定的目錄术唬。
WebApp ClassLoader:Java語言編寫薪伏,org.apache.catalina.loader.WebappClassLoader實現(xiàn)(在tomcat源碼中),負責(zé)加載Tomcat目錄webapps文件下中應(yīng)用里的/WEB-INF/lib和/WEB-INF/class.
Tomcat目錄結(jié)構(gòu)中粗仓,有三組目錄(/common/,/server/和shared/)可以存放公用Java類庫,以及第四組Web應(yīng)用程序目錄/WEB-INF/:
放置在common目錄中:類庫可被Tomcat和所有的Web應(yīng)用程序共同使用嫁怀。
放置在server目錄中:類庫可被Tomcat使用设捐,但對所有的Web應(yīng)用程序都不可見。
放置在shared目錄中:類庫可被所有的Web應(yīng)用程序共同使用塘淑,但對Tomcat自己不可見萝招。
放置在/WebApp/WEB-INF目錄中:類庫僅可以被此Web應(yīng)用程序使用,對Tomcat和其他Web應(yīng)用程序都不可見存捺。
值得注意的是槐沼,在Tomcat的6.0版本之后,只有在tomcat/conf/catalina.properties文件中給server.loader屬性和share.loader屬性賦值后捌治,才會去建立Catalina ClassLoader和Shared ClassLoader兩個類加載器的實例岗钩,否則在用到這兩個類加載器的地方都會用Common ClassLoader的實例代替(org.apache.catalina.loader.StandardClassLoader),而/common肖油、/server和/shared目錄在Tomcat6.0之后就不存在了兼吓,取而代之的是/lib目錄。
Tomcat類加載源碼
說完了森枪,Tomcat類加載的結(jié)構(gòu)视搏。本小節(jié),具體說下Tomcat的類加載機制县袱,想要說清楚浑娜,那必須還得從Tomcat源碼來入手。
找到org.apache.catalina.startup.Bootstrap類式散,發(fā)現(xiàn)該類中有main方法筋遭,沒錯這就是Tomcat的起點。
public static void main(String args[]) {
if (daemon == null) {
// 創(chuàng)建Bootstrap對象:
Bootstrap bootstrap = new Bootstrap();
try {
// bootstrap初始化
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
// command為start杂数,代表著啟動:
daemon.setAwait(true);
daemon.load(args);
daemon.start();
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null==daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
.....省略
}
}
在main方法中宛畦,我們首先創(chuàng)建了一個Bootstrap對象,并調(diào)用其init()方法揍移。
在初始化結(jié)束后,判斷虛擬機傳入的指令反肋,通常為“start”那伐,開始加載、啟動流程石蔗,調(diào)用了daemon.load(args)罕邀、daemon.start()方法來實現(xiàn)。
public void init() throws Exception {
setCatalinaHome();
setCatalinaBase();
// Tomcat類加載器初始化
initClassLoaders();
// 設(shè)置線程上下文類加載器:
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
if (log.isDebugEnabled())
log.debug("Loading startup class");
// 創(chuàng)建Catalina對象:
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.newInstance();
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);
// 將Catalina對象中的 parentClassLoader屬性設(shè)置為sharedLoader類加載器
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
在Bootstrap初始化方法中养距,有兩行值得注意诉探,(1)initClassLoaders:初始化Tomcat類加載器、(2)Thread.currentThread().setContextClassLoader(catalinaLoader):將catalinaLoader設(shè)置為線程上下文類加載器(打破了雙親委派模型)棍厌;
緊接著肾胯,又通過反射的方式竖席,創(chuàng)建了Catalina對象,并對Catalina對象中的parentClassLoader屬性重新賦值敬肚,賦值為sharedLoader類加載器毕荐,也就是common ClassLoader。注意艳馒,此處賦值的目的是為了后續(xù)創(chuàng)建WebApp ClassLoader使用憎亚。
private void initClassLoaders() {
try {
// Common類加載器
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
commonLoader=this.getClass().getClassLoader();
}
// Catalina類加載器
catalinaLoader = createClassLoader("server", commonLoader);
// Shared類加載器
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
在初始化Tomcat類加載器方法中,我們總共創(chuàng)建了3大類加載器弄慰,分別為commonLoader第美、catalinaLoader、sharedLoader陆爽。默認情況下什往,在Tomcat6.0之后,commonLoader墓陈、catalinaLoader恶守、sharedLoader對象相同,并且均為org.apache.catalina.loader.StandardClassLoader實例贡必。
Tomcat類加載器初始化時兔港,很明確是指出catalina ClassLoader和shared ClassLoader類加載器的父類加載器為common ClassLoader類加載器,而common ClassLoader的父類加載器傳值為null仔拟,繼續(xù)調(diào)用createClassLoader()方法:
private ClassLoader createClassLoader(String name, ClassLoader parent)throws Exception {
// 從conf/catalina.properties中獲取具體的key:
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
// 將common.loader中的值衫樊,替換成服務(wù)器上的絕對路徑,并以利花;相隔
value = replace(value);
List<Repository> repositories = new ArrayList<Repository>();
// 遍歷value中的值:
StringTokenizer tokenizer = new StringTokenizer(value, ",");
while (tokenizer.hasMoreElements()) {
String repository = tokenizer.nextToken().trim();
if (repository.length() == 0) {
continue;
}
...省略
// 將value中的值科侈,組裝成Repository對象,并存入到List集合中
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);
}
在創(chuàng)建ClassLoader方法中炒事,我們解析catalina.properties文件中的common.loader屬性臀栈,默認值為${catalina.base}/lib,${catalina.base}/lib/.jar,${catalina.home}/lib,${catalina.home}/lib/.jar**,將默認值中的${}進行替換挠乳,最終得到了服務(wù)器上的絕對路徑权薯,例如:(catalina.base,catalina.home是環(huán)境變量中的值,可通過System.getProperty("xxx")獲得)
F:\framework_code\apache-tomcat-7.0.88-src/lib,
F:\framework_code\apache-tomcat-7.0.88-src/lib/*.jar,
F:\framework_code\apache-tomcat-7.0.88-src/lib,
F:\framework_code\apache-tomcat-7.0.88-src/lib/*.jar
在得到這些絕對路徑后睡扬,進行Repository對象組裝盟蚣,并添加到ArrayList集合中來。最后卖怜,調(diào)用ClassLoaderFactory.createClassLoader(repositories, parent)生成具體的ClassLoader實現(xiàn)類屎开。
在創(chuàng)建common ClassLoader中:commonLoader = createClassLoader("common", null),傳遞父類加載器為parent = null马靠,當(dāng)實際創(chuàng)建類加載器對象時奄抽,會以系統(tǒng)類加載器(App ClassLoader)作為common ClassLoader的父類加載器蔼两。
說完了Common ClassLoader、Catalina ClassLoader如孝、Shared ClassLoader的實例化宪哩,我們接下來在來看看WebApp ClassLoader在Tomcat中是如何初始化的!5谖锁孟!
之前,在創(chuàng)建Catalina對象時候茁瘦,我們說到了對Catalina對象中的parentClassLoader屬性重新賦值品抽,是為了創(chuàng)建WebApp ClassLoader時使用。具體如何甜熔,我們來看下源碼T残簟!
由于Tomcat代碼太多腔稀,本次不展示代碼的流轉(zhuǎn)過程盆昙,直接來看源碼!
org.apache.catalina.loader.WebappClassLoader創(chuàng)建源碼焊虏,存在于org.apache.catalina.loader.WebappLoader類的createClassLoader()方法中:
private WebappClassLoaderBase createClassLoader() throws Exception {
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
if (parentClassLoader == null) {
parentClassLoader = container.getParentClassLoader();
}
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}
loaderClass是WebappLoader的實例變量淡喜,其值為org.apache.catalina.loader.WebappClassLoader,通過反射調(diào)用了WebappClassLoader的構(gòu)造函數(shù)诵闭,然后傳遞了sharedLoader(也就是common ClassLoader)作為其父親加載器炼团。
來看下org.apache.catalina.loader.WebappClassLoader的構(gòu)造器:
public WebappClassLoader(ClassLoader parent) {
super(parent);
}
由于WebappClassLoader繼承org.apache.catalina.loader.WebappClassLoaderBase類,所以來看看WebappClassLoaderBase類的構(gòu)造:
public WebappClassLoaderBase(ClassLoader parent) {
super(new URL[0], parent);
ClassLoader p = getParent();
if (p == null) {
p = getSystemClassLoader();
}
this.parent = p;
ClassLoader j = String.class.getClassLoader();
if (j == null) {
j = getSystemClassLoader();
while (j.getParent() != null) {
j = j.getParent();
}
}
this.j2seClassLoader = j;
securityManager = System.getSecurityManager();
if (securityManager != null) {
refreshPolicy();
}
}
將父類加載傳入疏尿,賦值給parent屬性切端,為sharedLoader(也就是common ClassLoader)吩抓。此外捆姜,繼續(xù)對j2seClassLoader屬性賦值炭臭,通過遍歷獲取到了sun.misc.Launcher.ExtClassLoader類加載。
至此敌呈,整個Tomcat類加載器初始化流程結(jié)束嚼鹉。
工作中遇到的類加載問題
上面我們說了Tomcat的類加載機制。
其中驱富,關(guān)于Tomcat的類加載器加載路徑,筆者提到了跟/conf/catalina.properties文件有關(guān)匹舞。
在該文件中褐鸥,可以自定義common.loader屬性,修改此屬性后赐稽,其他目錄下的.jar包叫榕、.class文件可以通過Tomcat的common ClassLoader加載進來浑侥。
在筆者工作的環(huán)境中,Tomcat的此屬性就被進行了修改晰绎,將應(yīng)用中maven所有依賴的jar包都放到了common.loader所配置的目錄下寓落,這樣做的結(jié)果就是:common ClassLoader將加載應(yīng)用中依賴的jar包,而應(yīng)用本身的WebappClassLoader失去了原本存在的意義荞下。不過伶选,這么做倒沒什么關(guān)系,只是在代碼開發(fā)時尖昏,會出現(xiàn)莫名的異常仰税。
截圖中,就是筆者所在公司的項目結(jié)構(gòu)抽诉,使用了maven的模塊化功能陨簇。
將hessian打成war包,進行Tomcat部署迹淌,hessian中如果沒有servlet的話河绽,基本上沒有業(yè)務(wù)代碼。
facade是對外的接口層唉窃,core是底層實現(xiàn)耙饰。
hessian依賴facade、core模塊句携。
編寫測試代碼:
在hessian模塊下創(chuàng)建servlet類榔幸,并調(diào)用core模塊中的任意一個類。
public class TestClassLoaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
super.service(req, resp);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
super.doGet(req, resp);
TestUtil.test();
}
}
在core模塊中矮嫉,類加載hessian模塊下的創(chuàng)建servlet類削咆,使用Class.forName("xxxx")即可。
public static void test(){
try{
Class clazz = Class.forName("com.jiaboyan.hessian.TestClassLoaderServlet");
logger.info("要加載的類為:"蠢笋,clazz);
}catch (Throwable th){
logger.error("異常",th);
}
}
在本地對hessian進行打包拨齐,結(jié)果如下:
項目中的依賴,以及core模塊昨寞、facade模塊都會被打到hessian.war/WEB-INF/lib下瞻惋。
本地tomcat啟動,可以在idea中啟動援岩,也可以放到Tomcat目錄中去啟動也行歼狼。
輸出結(jié)果如下:
要加載的類為:class com.jiaboyan.hessian.TestClassLoaderServlet
一切都是如此的正常。
此時享怀,你的新功能開發(fā)完了羽峰,自測完了,你需要部署到測試服務(wù)器中。利用jenkins進行部署梅屉,公司的jenkins進行了修改值纱,在部署構(gòu)建過程中,會將war包中/WEB-INF/lib下的所有jar包坯汤,拷到Tomcat/commonlib目錄下虐唠,并刪除/WEB-INF/lib下的所有jar包。
而我們公司的Tomcat又對/conf/catalina.properties中的common.loader進行了修改:
common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar,${catalina.home}/commonlib/*.jar,${catalina.home}/commoncfg
通過common.loader的配置可知惰聂,common ClassLoader會加載commonlib目錄下的.jar包疆偿。
如果此時你發(fā)現(xiàn)了問題,那么說明你已經(jīng)理解了Tomcat的類加載機制庶近。
由此可見翁脆,對于core模塊、facade模塊下的代碼鼻种,均有common ClassLoader進行加載反番,而hessian模塊中的代碼是由WebApp ClassLoader進行加載。并且叉钥,common ClassLoader是WebApp ClassLoader的父類加載器罢缸,父類加載無法加載子類加載器的資源,當(dāng)測試服務(wù)器中代碼執(zhí)行時候投队,common ClassLoader無法加載WebApp ClassLoader所擁有的類枫疆,結(jié)果就是類加載異常。