2005 年 6 月 01 日 本文通過開發一個JSP 編輯器外掛程式的樣本,介紹了 Eclipse 中設定 JSP 斷點的方法,以及如何遠端偵錯 JSP。作為基礎知識,本文的前兩部分描述了 JAVA Debug 和 JSR-45 的基本原理。
環境要求: 本文的代碼是在 Eclipse3.0.0,JDK1.4.2 和 Tomcat5.0.5 上測試過的。
JAVA 調試架構(JPDA)簡介
JPDA 是一個多層的調試架構,包括 JVMDI、JDWP、JDI 三個層次。JAVA 虛擬機器提供了 JPDA 的實現。其開發工具作為調試用戶端,可以方便的與虛擬機器通訊,進行調試。Eclipse 正是利用 JPDA 調試 JAVA 應用,事實上,所有 JAVA 開發工具都是這樣做的。SUN JDK 還帶了一個比較簡單的調試工具以及樣本。 JVMDI 定義了虛擬機器需要實現的本地介面 JDWP 定義了JVM與調試用戶端之間的通訊協議
調試用戶端和JVM 既可以在同一台機器上,也可以遠端偵錯。JDK 會包含一個預設的實現 jdwp.dll,JVM 允許靈活的使用其他協議代替 JDWP。SUN JDK 有兩種方式傳輸通訊協議:Socket 和共用記憶體(後者僅僅針對 Windows),一般我們都採用 Socket 方式。
你可以用下面的參數,以偵錯模式啟動JVM
JDI 則是一組JAVA介面
如果是一個 JAVA 的調試用戶端,只要實現 JDI 介面,利用JDWP協議,與虛擬機器通訊,就可以調用JVMDI了。
下圖為 JPDA 的基本架構:
參見:http://java.sun.com/j2se/1.4.2/docs/guide/jpda/architecture.html
Eclipse作為一個基於 JAVA 的調試用戶端,利用 org.eclipse.jdt.debug Plugin 提供了JDI 的具體實現。JDI 介面主要包含下面 4 個包
com.sun.jdi com.sun.jdi.connect com.sun.jdi.event com.sun.jdi.request
本文不對 JDI 進行深入闡述,這裡重點介紹 JDI 中與斷點相關的介面 com.sun.jdi
主要是JVM(VirtualMachine) 線程(ThreadReference) 調用棧(StackFrame) 以及類型、執行個體的描述。利用這組介面,調試用戶端可以用類似類反射的方式,得到所有類型的定義,動態調用 Class 的方法。 com.sun.jdi.event
封裝了JVM 產生的事件, JVM 正是將這些事件通知給調試用戶端的。例如 BreakpointEvent 就是 JVM 執行到斷點的時候,發出的事件;ClassPrepareEvent就是 Class 被載入時發出的事件。 com.sun.jdi.request
封裝了調試用戶端可以向 JVM發起的請求。例如 BreakpointRequest 向 JVM 發起一個添加斷點的請求;ClassPrepareRequest 向 JVM 註冊一個類載入請求,JVM 在載入指定 Class 的時候,就會發出一個 ClassPrepareEvent 事件。
JSR-45規範
JSR-45(Debugging Support for Other Languages)為那些非 JAVA 語言寫成,卻需要編譯成 JAVA 代碼,運行在 JVM 中的程式,提供了一個進行調試的標準機制。也許字面的意思有點不好理解,什麼算是非 JAVA 語言呢。其實 JSP 就是一個再好不過的例子,JSR-45 的範例就是一個 JSP。
JSP的調試一直依賴於具體應用伺服器的實現,沒有一個統一的模式,JSR-45 針對這種情況,提供了一個標準的模式。我們知道,JAVA 的調試中,主要根據行號作為標誌,進行定位。但是 JSP 被編譯為 JAVA 代碼之後,JAVA 行號與 JSP 行號無法一一對應,怎樣解決呢。
JSR-45 是這樣規定的:JSP 被編譯成 JAVA 代碼時,同時產生一份 JSP 檔案名稱和行號與 JAVA 行號之間的對應表(SMAP)。JVM 在接受到調試用戶端請求後,可以根據這個對應表(SMAP),從 JSP 的行號轉換到 JAVA 代碼的行號;JVM 發出事件通知前, 也根據對應表(SMAP)進行轉化,直接將 JSP 的檔案名稱和行號通知調試用戶端。
我們用 Tomcat 5.0 做個測試,有兩個 JSP,Hello.jsp 和 greeting.jsp,前者 include 後者。Tomcat會將他們編譯成 JAVA 代碼(Hello_jsp.java),JAVA Class(Hello_jsp.class) 以及 JSP 檔案名稱/行號和 JAVA 行號之間的對應表(SMAP)。
Hello.jsp:
greeting.jsp:
1 Hello There!<P> 2 Goodbye on <%= new java.util.Date() %>
JSP 編譯後產生的Hello_jsp.java 如下:
Tomcat 又將這個 JAVA 代碼編譯為 Hello_jsp.class,他們位於: $Tomcat_install_path$/work/Standalone/localhost/_ 目錄下。但是 JSP 檔案名稱/行號和 JAVA 行號的對應表(以下簡稱SMAP) 在哪裡呢。答案是,它儲存在 Class 中。如果用 UltraEdit 開啟這個 Class 檔案,就可以找到 SourceDebugExtension 屬性,這個屬性用來儲存 SMAP。
JVM 規範定義了 ClassFile 中可以包含 SourceDebugExtension 屬性,儲存 SMAP:
我用 javassist 做了一個測試(javassist可是一個好東東,它可以動態改變Class的結構,JBOSS 的 AOP就利用了javassist,這裡我們只使用它讀取ClassFile的屬性)
這段代碼顯示了SourceDebugExtension 屬性,你可以看到SMAP 的內容。編譯JSP後,SMAP 就被寫入 Class 中, 你也可以利用 javassist 修改 ClassFile 的屬性。
下面就是 Hello_jsp.class 中儲存的 SMAP 內容:
首先註明JAVA代碼的名稱:Hello_jsp.java,然後是 stratum 名稱: JSP。隨後是兩個JSP檔案的名稱 :Hello.jsp、greeting.jsp。兩個JSP檔案共10行,產生的Hello_jsp共69行代碼。最後也是最重要的內容就是源檔案檔案名稱/行號和目標檔案行號的對應關係(*L 與 *E之間的部分)
在規範定義了這樣的格式:
源檔案行號 # 源檔案代號,重複次數 : 目標檔案開始行號,目標檔案行號每次增加的數量
(InputStartLine # LineFileID , RepeatCount : OutputStartLine , OutputLineIncrement)
源檔案行號(InputStartLine) 目標檔案開始行號(OutputStartLine) 是必須的。下面是對這個SMAP具體的說明:
開發一個JSP編輯器
Eclipse 提供了 TextEditor,作為文字編輯器的父類。由於 Editor 的開發不是本文的重點,不做具體論述。我們可以利用 Eclipse 的 Plugin 項目嚮導,產生一個簡單的 JSP 編輯器:
(1)點擊 File 菜單,New -> Project -> Plug-in Project ;
(2)輸入項目名稱 JSP_DEBUG,下一步;
(3)輸入 plugin ID : com.jsp.debug
Plugin Class name : com.jsp.debug.JSP_DebugPlugin
(4)選擇用模板建立
使用 Plug-in with editor,輸入
Java Package Name :com.jsp.editors
Editor Class Name :JSPEditor
File extension :jsp
一個 jsp editor 就產生了。
運行這個Plugin,建立一個JAVA項目,建立一個 Hello.jsp 和 greeting.jsp,在 Navigator 視圖雙擊 jsp,這個editor就開啟了。
在JSP編輯器中設定斷點
在編輯器中添加斷點的操作方式有兩種,一種是在編輯器左側垂直尺規上雙擊,另一種是在左側垂直尺規上點擊滑鼠右鍵,選擇菜單"添加/刪除斷點"。
在 Eclipse 的實現中,添加斷點實際上就是為 IFile 添加一個marker ,類型是IBreakpoint.BREAKPOINT_MARKER,然後將斷點註冊到 BreakpointManager。
BreakpointManager 將產生一個 BreakpointRequest,通知正在啟動並執行JVM Target,如果此時還沒有啟動 JVM,會在 JVM 啟動的時候,將所有斷點一起通知 JVM Target。
添加斷點使用一個 AbstractRulerActionDelegate,重載 createAction 方法,返回一個 IAction ManageBreakpointRulerAction動作:
為了將 ManageBreakpointRulerActionDelegate 添加到文字編輯器左側尺規的滑鼠右鍵菜單,並且能夠處理左側尺規的滑鼠雙擊事件,在 plugin.xml 中加入定義。
處理雙擊事件:
添加右鍵菜單:
ManageBreakpointRulerAction 是實際添加斷點的Action,實現了 IUpdate 介面,這個Action的工作,就是判斷當前選中行是否存在斷點類型的 Marker,如果不存在建立一個,如果存在,將它刪除。
update 方法會在點擊時首先調用,這時就可以收集當前選中行是否有marker了(調用fetchBPMarkerList方法),如果有,就儲存在 變數allMarkers 中。由於ManageBreakpointRulerAction每一次都產生一個新的執行個體,因此不會產生衝突。
下面是update的調用棧,可以看出,update方法是在滑鼠點擊事件中被調用的:
updae被調用後,會執行 run 方法,就可以根據 allMarkers.isEmpty() 確定要刪除還是添加 marker 了。
添加斷點的時候,首先利用 IVerticalRulerInfo,擷取滑鼠點擊的行號,根據行號,從 Document 模型中取得該行的描述IRegion,得到開始字元位置和結束字元位置,建立一個 JSP 斷點。
註冊 JSP 斷點為支援 JSR-45 規範,Eclipse 中提供了 JavaStratumLineBreakpoint。不過它目前是一個 internal 的實現,在以後的版本中不能保證不作修改。這裡為了簡單起見,直接從 JavaStratumLineBreakpoint 繼承。
查看 JavaStratumLineBreakpoint 的原始碼可以知道,建立 JavaStratumLineBreakpoint 的時候做了兩件事情:
(1) 建立斷點類型的 marker, 並且設定了marker的屬性
resource.createMarker(markerType);
(2) 將斷點註冊到斷點管理器
DebugPlugin.getDefault().getBreakpointManager().addBreakpoint(this); 斷點管理器負責產生一個 BreakpointRequest,通知正在啟動並執行JVM Target 如果此時還沒有啟動 JVM,會在 JVM 啟動的時候,將所有斷點一起通知 JVM Target。
下面是 JavaStratumLineBreakpoint 建構函式中的代碼:
移除斷點的時候,根據 marker 找到相應的 IBreakpoint,從 BreakpointManager 中移除 BreakpointManager 會自動刪除 marker,通知 JVM Target。
JSPBreakpoint 重載了父類的addToTarget(JDIDebugTarget target) 方法。重載這個方法的目的是根據不同的應用伺服器,設定不同的 referenceTypeName和sourcePath。我們知道,每種應用伺服器編譯 JSP 產生Java Class 名稱的規則都不相同,例如Tomcat編譯Hello.jsp 產生的Java 類名為 org.apache.jsp. Hello_jsp,而WebSphere6.0 卻是 com.ibm._jsp._Hello。只有確定伺服器類型,才能知道referenceTypeName 和souecePath應該是什麼。目前通過啟動 JVM 時target 名稱來判斷應用伺服器類型: String targetString = target.getLaunch().getLaunchConfiguration().getName(); 如果targetString 包含 Tomcat ,就認為是 Tomcat。
產生 referenceTypeName 後首先建立一個 ClassPrepareRequest 通知,然後從vm中取出所有的classes,如果是當前的 Class,再建立一個添加斷點通知。之所以這樣做,是因為有可能這個 Class 還沒有被 JVM 載入,直接通知 JVM 沒有任何意義。在 Class 被載入的時候,JVM 會通知 Eclipse,這個時候,才產生添加斷點通知。需要指出的是,本文範例程式碼擷取 referenceTypeName 的方法不是很完善:
(1) 僅僅實現了Tomcat 讀者有興趣可以實現更多的Web容器,例如 JBoss3 以上,WebSphere6.0
(2) 一些特殊情況沒有處理例如 路徑名為package的jsp,路徑名或檔案名稱帶有數位jsp
調試JSP
現在我們可以調試 JSP 了。
(1)運行 JSP_DEBUG plugin
首先在 run -> run 中添加一個 Run-time Workbench,點擊 run 按鈕,Eclipse 的Plugin開發環境會啟動一個新的Eclipse,這個新啟動的 Eclipse 中,我們建立的 JSP_DEBUG plugin 就可以使用了。建立 一個 JAVA 項目 Test (注意,一定要是JAVA項目),建立一個 Hello.jsp 和 greeting.jsp,開啟Hello.jsp,在編輯器左側尺規雙擊,就出現了一個斷點。
(2)以 Debug 模式啟動Tomcat:
windows 開始 -> 運行,鍵入 cmd,啟動一個命令列視窗:
cd E:/Tomcat5_0_5/bin
(我的 Tomcat 安裝在 E:/Tomcat5_0_5 目錄,JDK 安裝在 D:/j2sdk1.4.2)
-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=8888,server=y,suspend=n 表示以調試方式啟動,連接埠號碼是 8888 classpath中要加入 D:/j2sdk1.4.2/lib/tools.jar,因為我是 Tomcat5.0.5,如果是5.5就不需要了。
(3) 測試Hello.jsp
將 Hello.jsp 和 greeting.jsp 拷貝到 E:/Tomcat5_0_5/webapps/ROOT 目錄,從瀏覽器訪問 Hello.jsp http://localhost:8000/Hello.jsp。成功的話就可以繼續下面的工作了,如果失敗,檢查你的Tomcat設定。
(4)啟動遠端偵錯
在 Eclipse 中啟動遠端偵錯,將 Eclipse 作為一個 Debug 用戶端,串連到 Tomcat 。在 Java 透視圖中,點擊 Run -> Debug ,添加一個 Remote Java Application,名稱是 Start Tomcat Server(不能錯,因為我們要根據這個名稱,判斷當前的 Web Server 類型)
project是建立的 Test 項目
Port 為 8888,和啟動 Tomcat 時設定的一樣
點擊 Debug 按鈕,就可以串連到 Tomcat 上了。切換到 Debug 透視圖,在Debug 視圖中,能夠看到所有 Tomcat 中線程的列表。
(5)調試Hello.jsp
為 Hello.jsp 添加斷點,然後從瀏覽器訪問Hello.jsp,就可以在斷點處掛起了。你可以使用逐步執行,也可以在Variables視圖查看jsp中的變數資訊。
由於 Eclipse 自身的實現,現在的 JSP Editor 有一個問題,逐步執行到 include jsp 行後,會從Hello.jsp的1行再次執行。這是因為 Eclipse JDT Debug視圖緩衝了 StackFrame 中已經開啟的Editor,StackFrame不改變時,不會再重新計算當前調試的是否是其他Resource。本來應該開啟 greeting.jsp的,現在卻從 Hello.jsp 的第 1 行開始執行了。
結束語
很多整合式開發環境都支援 JSP 的調試,在 Eclipse 中也有 MyEclipse 這樣的外掛程式完成類似的功能。但是在 JSR-45 規範產生前,每種應用伺服器對 JSP Debug 的實現是不一樣的,例如 WebSphere 5 就是在 JSP 編譯產生的 JAVA 代碼中加入了兩個數組,表示源檔案和行號的對應資訊。Tomcat 率先實現了 JSR-45 規範,WebSphere 6.0 現在也採取這種模式, 有興趣的話,可以查看 WebSphere 6.0 編譯的 Class,和 Tomcat 不一樣,SMAP 檔案會和java代碼同時產生。
但是啟動server前,需要設定 JVM 參數 was.debug.mode = true
同時在 ibm-web-ext.xmi 中設定
利用本文的基本原理,我們也可以開發其他基於 JAVA 指令碼語言的編輯器(例如 Groovy),為這個編譯器加入 Debug 的功能。
下載
名字 |
大小 |
下載方法 |
debug.zip |
260 KB |
HTTP |
jsp_debug_project.zip |
280 KB |
HTTP |
|
|
關於下載方法的資訊 |
|
|
Get Adobe® Reader® |
參考資料 JPDA
http://java.sun.com/j2se/1.4.2/docs/guide/jpda/
JSR-45
http://www.jcp.org/en/jsr/detail?id=45
IBM 專家 Scott Johnson 的文章
WebSphere Application Server V6 中的 JavaServer Pages
http://www-128.ibm.com/developerworks/cn/websphere/techjournal/0412_johnson/0412_johnson.html