1. 程式人生 > >超越reloadable=true, 在Tomcat執行時動態過載類(version 5.0.28)

超越reloadable=true, 在Tomcat執行時動態過載類(version 5.0.28)

  1. 為什麼寫這篇文件?

使用過hibernate, spring或其他大型元件,寫過50個類以上的網路應用程式(web application)的開發者應該知道,當系統中有很多類時,如果開啟了Tomcat的reloadable=true,那麼每當相關檔案改變時,Tomcat會停止web app並釋放記憶體,然後重新載入web app.這實在是個浩大的工程。

所以我總是在想如果能有隻過載某幾個類的功能,將極大的滿足我這個即時除錯狂。

去年我在論壇上發帖,才發現已經有一些應用伺服器具有了這個功能,比如WebLogic, WebSphere, 等等。好像還有一個很酷的名字,叫開發模式。看來我還是孤陋寡聞了點。

當然很多人都是在Tomcat上開發,包括我。我很喜歡它的輕小,那些大記憶體和高CPU消耗的應用伺服器不愧為硬體殺手,沒理由不改進Tomcat :)

  1. 最終實現功能

我沒有時間去研究Tomcat的檔案監聽機制,也沒時間去把他寫成”開發模式”這麼完整的功能,我最終實現的是,實現過載功能的測試jsp--很抱歉我還是沒辦法寫得更完整。當然,你可以在這個基礎上進行改進。

  1. 閱讀須知

閱讀本文,你應該具備以下知識

最好在你的電腦上安裝ant,因為Tomcat原始碼包使用ant從網際網路獲得依賴包。不過我也是修改了一個錯誤才使它完全編譯通過。

當然,你也可以用其他IDE

工具檢查並新增依賴包,在IDE中,其實你只需要新增jar直到使org.apache.catalina.loader.WebappClassLoader無錯即可。

  1. 修改過程

    1. 說明

新新增的程式碼請新增到java檔案的末尾,因為我在說明行數的時候,儘量符合原始行數

    1. web app類載入器

Tomcat中,org.apache.catalina.loader.WebappClassLoaderweb app的類載入器,所以需要修改它實現過載功能。

    1. 資源列表

WebappClassLoader中,有一個Map型別屬性resourceEntries,它記載了web appWEB-INF/classes目錄下所載入的類,因此當我們需要過載一個類時,我們需要先將它在resourceEntries裡刪除,我編寫了一個方法方便呼叫:

publicboolean removeResourceEntry(String name) {

if (resourceEntries.containsKey(name)) {

         resourceEntries.remove(name);

returntrue;

     }

returnfalse;

}

    1. 是否過載標誌

WebappClassLoader需要知道載入一個類是否使用過載的方式。所以我建立一個boolean 型別的屬性和實現它的getter/setter方法:

privateboolean isReload = false;

publicboolean isReload() {

return isReload;

      }

publicvoid setReload(boolean isReload) {

this.isReload = isReload;

      }

    1. 動態類載入器

根據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);

}

}

    1. webappClassLoader中新增DynamicClassLoader

      1. 新增屬性

private DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(this);

      1. 新增重建方法,以便需要再次過載時替換掉上次的類載入器物件

publicvoid reCreateDynamicClassLoader() {

dynamicClassLoader = new DynamicClassLoader(this);

}

    1. 修改呼叫點

      1. 832行,公開findClass方法

public Class findClass(String name) throws ClassNotFoundException {

      1. 1569行,新增如下一行程式碼。

if (isReload) removeResourceEntry(name);

      1. 1577行,這裡好像是一個bug,具體原因我忘了-_-||

if ((entry == null) || (entry.binaryContent == null))

改為

if ((entry == null) || (entry.loadedClass == null && entry.binaryContent == null))

      1. 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);

    1. 測試程式碼

      1. 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);

%>

      1. org.AClass

package org;

publicclass AClass {

public AClass() {

// 修改輸出內容確認Tomcat重新載入了類

System.out.println("AClass v3");

}

publicvoid createBClass() {

new BClass();

}

}

      1. org.BClass

package org;

publicclass BClass {

public BClass() {

//修改輸出內容確認Tomcat重新載入了類

System.out.println("BClass v1");

}

}

    1. 測試步驟

      1. 按照上述步驟修改Tomcat原始碼並編譯。

      2. winzip/winrar/file-roller開啟$CATALINA_HOME/server/lib/catalina.jar。把前面編譯完成後的org.apache.catalina.loader目錄下的class檔案覆蓋jar中同名檔案。

      3. 編譯org.AClassorg.BClass

      4. 啟動Tomcat並在瀏覽器中開啟測試頁http://localhost:8080/test.jsp

      5. 修改org.AClass中的System.out.println();語句並重編譯類。

      6. 按下F5按鍵重新整理瀏覽器。

      7. 檢視Tomcat控制檯是否輸出了不同的語句?

      8. Good Luck! :)))