為什麼寫這篇文檔?
使用過hibernate, spring或其他大型組件,寫過50個類以上的網路應用程式(web application)的開發人員應該知道,當系統中有很多類時,如果開啟了Tomcat的reloadable=true,那麼每當相關檔案改變時,Tomcat會停止web app並釋放記憶體,然後重新載入web app.這實在是個浩大的工程。
所以我總是在想如果能有只重載某幾個類的功能,將極大的滿足我這個即時調試狂。
去年我在論壇上發帖,才發現已經有一些應用伺服器具有了這個功能,比如WebLogic, WebSphere, 等等。好像還有一個很酷的名字,叫開發模式。看來我還是孤陋寡聞了點。
當然很多人都是在Tomcat上開發,包括我。我很喜歡它的輕小,那些大記憶體和高CPU消耗的應用伺服器不愧為硬體殺手,沒理由不改進Tomcat 。
最終實現功能
我沒有時間去研究Tomcat的檔案監聽機制,也沒時間去把他寫成”開發模式”這麼完整的功能,我最終實現的是,實現重載功能的測試jsp--很抱歉我還是沒辦法寫得更完整。當然,你可以在這個基礎上進行改進。
閱讀須知
閱讀本文,你應該具備以下知識
jvm 規範有關類載入器的章節
http://java.sun.com/docs/books/vmspec/2nd-edition/html/VMSpecTOC.doc.html
Tomcat 類載入機制
http://www.huihoo.org/apache/tomcat/
java 反射機制
http://java.sun.com/docs/books/tutorial/reflect/
ant
http://ant.apache.org/
(好象該網址被不定時封鎖,有時能上,有時不能)
最好在你的電腦上安裝ant,因為Tomcat源碼包使用ant從互連網獲得依賴包。不過我也是修改了一個錯誤才使它完全編譯通過。
當然,你也可以用其他IDE工具檢查並添加依賴包,在IDE中,其實你只需要添加jar直到使org.apache.catalina.loader.WebappClassLoader無錯即可。
修改過程
說明
新添加的代碼請添加到java檔案的末尾,因為我在說明行數的時候,盡量符合原始行數
web app類載入器
在Tomcat中,org.apache.catalina.loader.WebappClassLoader是web app的類載入器,所以需要修改它實現重載功能。
資源清單
在WebappClassLoader中,有一個Map類型屬性resourceEntries,它記載了web app中WEB-INF/classes目錄下所載入的類,因此當我們需要重載一個類時,我們需要先將它在resourceEntries裡刪除,我編寫了一個方法方便調用:
public boolean removeResourceEntry(String name) {
if (resourceEntries.containsKey(name)) {
resourceEntries.remove(name);
return true;
}
return false;
}
是否重載標誌
讓WebappClassLoader需要知道載入一個類是否使用重載的方式。所以我建立一個boolean 類型的屬性和實現它的getter/setter方法:
private boolean isReload = false;
public boolean isReload() {
return isReload;
}
public void 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
*
*/
public class 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);
}
return super.loadClass(name);
}
}
在webappClassLoader中添加DynamicClassLoader
添加屬性
private DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(this);
添加重建方法,以便需要再次重載時替換掉上次的類載入器對象
public void 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 = new byte[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;
public class AClass {
public AClass() {
// 修改輸出內容確認Tomcat重新載入了類
System.out.println("AClass v3");
}
public void createBClass() {
new BClass();
}
}
org.BClass
package org;
public class BClass {