超越reloadable=true, 在Tomcat執行時動態過載類(version 5.0.28)
-
為什麼寫這篇文件?
使用過hibernate, spring或其他大型元件,寫過50個類以上的網路應用程式(web application)的開發者應該知道,當系統中有很多類時,如果開啟了Tomcat的reloadable=true,那麼每當相關檔案改變時,Tomcat會停止web app並釋放記憶體,然後重新載入web app.這實在是個浩大的工程。
所以我總是在想如果能有隻過載某幾個類的功能,將極大的滿足我這個即時除錯狂。
去年我在論壇上發帖,才發現已經有一些應用伺服器具有了這個功能,比如WebLogic, WebSphere, 等等。好像還有一個很酷的名字,叫開發模式。看來我還是孤陋寡聞了點。
當然很多人都是在Tomcat上開發,包括我。我很喜歡它的輕小,那些大記憶體和高CPU消耗的應用伺服器不愧為硬體殺手,沒理由不改進Tomcat :)。
-
最終實現功能
我沒有時間去研究Tomcat的檔案監聽機制,也沒時間去把他寫成”開發模式”這麼完整的功能,我最終實現的是,實現過載功能的測試jsp--很抱歉我還是沒辦法寫得更完整。當然,你可以在這個基礎上進行改進。
-
閱讀須知
閱讀本文,你應該具備以下知識
最好在你的電腦上安裝ant,因為Tomcat原始碼包使用ant從網際網路獲得依賴包。不過我也是修改了一個錯誤才使它完全編譯通過。
當然,你也可以用其他IDE
-
修改過程
-
說明
-
新新增的程式碼請新增到java檔案的末尾,因為我在說明行數的時候,儘量符合原始行數
-
web app類載入器
-
在Tomcat中,org.apache.catalina.loader.WebappClassLoader是web app的類載入器,所以需要修改它實現過載功能。
-
資源列表
-
在WebappClassLoader中,有一個Map型別屬性resourceEntries,它記載了web app中WEB-INF/classes目錄下所載入的類,因此當我們需要過載一個類時,我們需要先將它在resourceEntries裡刪除,我編寫了一個方法方便呼叫:
publicboolean removeResourceEntry(String name) {
if (resourceEntries.containsKey(name)) {
resourceEntries.remove(name);
returntrue;
}
returnfalse;
}
-
是否過載標誌
-
讓WebappClassLoader需要知道載入一個類是否使用過載的方式。所以我建立一個boolean 型別的屬性和實現它的getter/setter方法:
privateboolean isReload = false;
publicboolean isReload() {
return isReload;
}
publicvoid setReload(boolean isReload) {
this.isReload = isReload;
}
-
動態類載入器
-
根據jvm類載入器規範,一個類載入器物件只能載入一個類1次,所以過載實際上是創建出另一個類載入器物件來載入同一個類。當然,我們不需要再建立一個WebappClassLoader,他太大而且載入規則很複雜,不是我們想要的,所以我們建立一個簡單的類載入器類org.apache.catalina.loader.DynamicClassLoader:
package org.apache.catalina.loader;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.util.*;
/**
*動態類載入器
*
*@author peter
*
*/
publicclass DynamicClassLoader extends URLClassLoader {
/* 父類載入器 */
private ClassLoader parent = null;
/* 已載入類名列表 */
private List classNames = null;
/**
*構造器
*
*@param parent
*父類載入器,這裡傳入的是WebappClassLoader
*/
public DynamicClassLoader(ClassLoader parent) {
super(new URL[0]);
classNames = new ArrayList();
this.parent = parent;
}
/**
*從類的二進位制資料中載入類.
*
*@param name
*類名
*@param classData
*類的二進位制資料
*@param codeSource
*資料來源
*@return 成功載入的類
*@throws ClassNotFoundException
*載入失敗丟擲未找到此類異常
*/
public Class loadClass(String name, byte[] classData, CodeSource codeSource) throws ClassNotFoundException {
if (classNames.contains(name)) {
// System.out.println("此類已存在,呼叫 loadClass 方法載入.");
return loadClass(name);
} else {
// System.out.println("新類, 記錄到類名列表,並用類定義方法載入類");
classNames.add(name);
return defineClass(name, classData, 0, classData.length, codeSource);
}
}
/* *
* 過載此方法,當要載入的類不在類名列表中時,呼叫父類載入器方法載入.
* @see java.lang.ClassLoader#loadClass(java.lang.String)
*/
public Class loadClass(String name) throws ClassNotFoundException {
if (!classNames.contains(name)) {
//System.out.println("不在類名列表中,呼叫父類載入器方法載入");
return parent.loadClass(name);
}
returnsuper.loadClass(name);
}
}
-
在webappClassLoader中新增DynamicClassLoader
-
新增屬性
-
-
private DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(this);
-
新增重建方法,以便需要再次過載時替換掉上次的類載入器物件
-
publicvoid reCreateDynamicClassLoader() {
dynamicClassLoader = new DynamicClassLoader(this);
}
-
修改呼叫點
-
第832行,公開findClass方法
-
-
public Class findClass(String name) throws ClassNotFoundException {
-
第1569行,新增如下一行程式碼。
-
if (isReload) removeResourceEntry(name);
-
第1577行,這裡好像是一個bug,具體原因我忘了-_-||
-
if ((entry == null) || (entry.binaryContent == null))
改為
if ((entry == null) || (entry.loadedClass == null && entry.binaryContent == null))
-
第1633~1636行
-
if (entry.loadedClass == null) {
clazz = defineClass(name, entry.binaryContent, 0, entry.binaryContent.length,
codeSource);
改為
byte[] classData = newbyte[entry.binaryContent.length];
System.arraycopy(entry.binaryContent, 0, classData, 0,
classData.length);
if (entry.loadedClass == null) {
clazz = isReload ?
dynamicClassLoader.loadClass(name,
classData, codeSource) :
defineClass(name,
classData, 0, classData.length, codeSource);
-
測試程式碼
-
test.jsp
-
-
我測試用的jsp為$CATALINA_HOME/webapps/ROOT/test.jsp,由於webapp裡面並不會顯式載入tomcat的核心類,所以我們需要用反射程式碼呼叫WebappClassLoader的方法。程式碼如下:
<%
ClassLoader loader = (Thread.currentThread().getContextClassLoader());
Class clazz = loader.getClass();
java.lang.reflect.Method setReload = clazz.getMethod("setReload", new Class[]{boolean.class});
java.lang.reflect.Method reCreate = clazz.getMethod("reCreateDynamicClassLoader", null);
java.lang.reflect.Method findClass = clazz.getMethod("findClass", new Class[]{String.class});
reCreate.invoke(loader, null);
setReload.invoke(loader, new Object[]{true});
Class A = (Class)findClass.invoke(loader, new Object[]{"org.AClass"});
setReload.invoke(loader, new Object[]{false});
A.newInstance();
// 如果你使用下面這行程式碼,當重編譯類時,請稍微修改一下呼叫它的jsp,讓jsp也重新編譯
//org.AClass a = (org.AClass)A.newInstance();
// 下面這些程式碼是測試當一個類不在DynamicClassLoader類名列表時的反應
//a.test();
//java.lang.reflect.Method test = a.getClass().getMethod("test", null);
//test.invoke(a, null);
%>
-
org.AClass
-
package org;
publicclass AClass {
public AClass() {
// 修改輸出內容確認Tomcat重新載入了類
System.out.println("AClass v3");
}
publicvoid createBClass() {
new BClass();
}
}
-
org.BClass
-
package org;
publicclass BClass {
public BClass() {
//修改輸出內容確認Tomcat重新載入了類
System.out.println("BClass v1");
}
}
-
測試步驟
-
按照上述步驟修改Tomcat原始碼並編譯。
-
用winzip/winrar/file-roller開啟$CATALINA_HOME/server/lib/catalina.jar。把前面編譯完成後的org.apache.catalina.loader目錄下的class檔案覆蓋jar中同名檔案。
-
編譯org.AClass和org.BClass
-
啟動Tomcat並在瀏覽器中開啟測試頁http://localhost:8080/test.jsp
-
修改org.AClass中的System.out.println();語句並重編譯類。
-
按下F5按鍵重新整理瀏覽器。
-
檢視Tomcat控制檯是否輸出了不同的語句?
-
Good Luck! :)))
-
-