避免不必要的JSP重新編譯

關於JavaServer頁面(JSP)新聞組的最常見的一個問題與重新編譯有關。不想重新編譯JSP,卻又不得不這樣做,這是許多開發人員所面對的煩惱。本文將描述造成重新編譯的場景,並從解釋WebLogic JSP容器的內部操作開始,介紹每個顯然“不受歡迎的”場景,並應用容器的過期檢查算法(Stale Checking Algorithm)。此外,本文還將討論控制JSP和servlet類重載的參數。對以生產模式下運行的服務器,極力推薦這麼做。

JSP容器的過期檢查機制
  在WebLogic中,JSP被編譯成.class文件。我們使用的術語過期檢查機制(Stale Checking Mechanism)指的是用來判斷某一特殊JSP .class文件是否比當前JSP文件更舊(“過期”)的邏輯。WebLogic JSP容器確保任何JSP及其相關文件只在修改後才能被重新編譯。查看生成的Java代碼是瞭解JSP容器內部操作的一個好方法。我們將採用一個JSP作爲例子,使用命令行JSP編譯器編譯它,並查看生成的源代碼。JSP編譯器(WebLogic.jspc)是隨標準WebLogic 服務器安裝工具箱一起提供的。

考慮一個稱爲foo.jsp的簡單JSP頁面:

A simple JSP page

  現在使用命令行JSP編譯器來編譯這個JSP,指定一個稱爲keepgenerated的選項,顧名思義,該選項爲JSP頁面生成相應的Java代碼,並將代碼保存在磁盤上。

java weblogic.jspc -keepgenerated -d ./WEB-INF/classes foo.jsp

[jspc]warning: expected file /WEB-INF/web.xml not found, 
   tag libraries cannot be resolved.
   <Jul 11, 2004 7:29:26 PM PDT> <Warning> <HTTP> 
   <BEA-101181> <Could not find web.
   xml under WEB-INF in the doc root: ..>

  編譯器在作爲上述選項指定的輸出目錄(-d)中生成.java文件及其對應的.class文件。它將生成的類文件放在稱爲jsp_servlet的包中,這恰巧是默認的JSP包前綴(除非在weblogic.xml中覆蓋),因此,生成的Java文件可在./WEB-INF/classes/jsp_servlet中找到,並且稱爲__foo.java。
  請注意,我們可忽略編譯器發出的關於未找到web.xml文件的警告,因爲此時我們並沒有真正使用標籤庫。
  在生成代碼(__foo.java)中,與我們的討論最相關的部份就是staticIsStale()方法,如下所示。

清單1. staticIsStale()方法

public static boolean _staticIsStale(weblogic.servlet.jsp.StaleChecker sci) {
   if (sci.isResourceStale("/foo.jsp", 1089594167518L, "8.1.2.0", 
                                            "America/Los_Angeles")) 
     return true;
   return false;
}
  從上面一小段代碼中顯然可以看出,調用weblogic.servlet.jsp.StaleChecker接口上的isResourceStale()方法是爲了確定JSP是否已修改過。isResourceStale()方法的參數如下所示,這些參數是按以下順序出現的:

 

1、 要檢查的資源,比如,/foo.jsp。
2、 JSP頁面的時間戳(長整型)。
3、 WebLogic Release Build版。
4、 當前機器的默認時區。

  JSP容器通過實現StaleChecker接口調用_staticIsStale()方法。該實現接收一個帶有清單1中所示參數的回調(isResourceStale())。有了這些參數,該實現可以僅接收所有必需的信息,以推斷給定資源是否過期。當資源(參數1)/foo.jsp的時間戳(參數2)比存儲在已編譯類文件中的時間戳還要新(參數更大)時,或者當發行版本不同時,JSP容器認爲JSP.class文件“過期”。

  讓我們看它的一些重要結論:

  • 因爲JSP頁面的時間戳保存在類文件內部,並且是在編譯時計算的,所以修改類文件的時間戳不會對過期檢查過程產生影響。(這是一種很常見的荒謬說法,但願上面的例子清楚地駁斥了它。)
  • 第4個參數,也就是時區,只在以存檔格式(.war)進行部署時使用。
  • WebLogic發行版本隨每個服務包改變,因此需要爲每個服務包重新編譯所有JSP。提出這個要求是爲了確保JSP類可以利用較新服務包或發行版本中的所有編譯器缺陷修復或所有JSP運行時更改。

靜態包含怎樣?
  人們會問的下一個合乎邏輯的問題是:即使只修改了特定JSP頁面的一個靜態包含文件,JSP也會重新編譯這個頁面嗎?回答是肯定的。即使是修改了像靜態包含這樣的相關文件,也要重新編譯整個頁面(稱爲“編譯單元”更合適)。要了解容器如何處理這種依賴性,請考慮以下包含名爲baz.inc的靜態包含文件的JSP。

清單2. foo.jsp

A simple jsp page.
<%@ include file="baz.inc"%>

 

清單3. baz.inc

--
Simple Static Include
--
  爲JSP編譯器對foo.jsp重運行上述命令行,現在會產生一個Java文件,它包含如下所示的一小段有趣代碼。如您所見,每個從屬物都是用_staticIsStale()方法處理的,這樣,即使修改了一個從屬物(這裏是baz.inc),也要重新編譯整個JSP或“編譯單元”。JSP容器期望根JSP頁面(foo.jsp)返回一個指示它是否過期的布爾值。因此,每個生成的類文件都會生成檢驗其所有相關文件的代碼。
public static boolean _staticIsStale(weblogic.servlet.jsp.StaleChecker sci) {
  if (sci.isResourceStale("/foo.jsp", 1089616972487L, "8.1.2.0", "America/Los_Angeles")) 
    return true;
  if (sci.isResourceStale("/baz.inc", 1089616984268L, "8.1.2.0", "America/Los_Angeles")) 
    return true;
  return false;
}

  總之,WebLogic JSP容器讓每個JSP .class維護自己的從屬物列表,並根據這個列表來存儲原始JSP(及其從屬物)的狀態(時間戳)。容器對JSP .class調用_staticIsStale()方法,然後JSP .class回調JSP容器,並通過weblogic.servlet.jsp.StaleChecker.isResourceStale()返回判斷單個資源是否過期所需的所有信息。這大大簡化了過期檢查任務,而且消除了在單獨某個位置上爲每個JSP維護時間戳的需要。

導致重新編譯JSP的場景
  我們已經分析了JSP容器執行過期檢查時要考慮的一些因素。現在,讓我們來看看重新編譯JSP的一些常見場景:

1. 使用構建腳本的文件副本可修改JSP的時間戳。這可導致重新編譯所有JSP。
  考慮一下所有JSP都位於名爲src的目錄中的場景。假定某一構建腳本複製了所有JSP,並將servlet Java文件編譯到構建目錄中。然後該腳本在src目錄之上運行weblogic.jspc,並將所有已編譯的JSP放入構建目錄中。在這裏,將JSP複製到構建目錄中很可能改變了JSP的時間戳(除非構建腳本使用cp –p/-m保留文件時間戳)。當該Web應用程序從構建目錄部署到服務器上時,要重新編譯所有JSP,因爲JSP類是用比已部署的JSP更舊的JSP(也就是這裏複製到構建目錄下的JSP)來編譯的。這是最常見的重新編譯的情況之一,它可通過確保執行復制操作時保留了文件時間戳來避免。

2. 修改weblogic.xml的packagePrefix參數將導致重新編譯。過期檢查機制負責查找特殊Web應用程序weblogic.xml文件中的packagePrefix,併爲/foo.jsp搜索名爲<packagePrefix>.__foo.class的類。假定我們使用weblogic.jspc 預構建所有JSP,將它們放在Web應用程序的WEB-INF/classes目錄中,然後我們將該Web應用程序歸檔(WAR)部署到服務器上。假定我們在這個Web應用程序中有一個名爲foo.jsp的JSP。在weblogic.xml中缺乏“packagePrefix”的情況下,過期檢查機制將查找jsp_servlet.__foo.class類。現在假定我們修改weblogic.xml並添加一個包前綴,比如說com.bar,然後將同一WAR重新部署到服務器。此時訪問foo.jsp將導致重新編譯JSP,因爲過期檢查機制將查找名爲“com.foo.__foo.class”的類。通過確保調用weblogic.jspc命令時使用了-package參數並使用了相同的包名稱,可避免這個問題。

3. 修改weblogic.xml的workingDir參數也會導致重新編譯。在這種情況下,除了通常的Web應用程序類路徑之外,JSP容器還將在新的“workingDir”中查找JSP類。因爲在部署新版本之前,原先使用的目錄中有JSP類,但JSP容器無法找到它們,因此將重新編譯請求的JSP。
  注意:場景2和場景3清楚地解釋了即使在修改weblogic.xml時,也需要重構建或預編譯Web應用程序。在完成所有修改後部署預編譯的WAR,確保不用重新編譯JSP。

4. 將預構建的WAR部屬到一個更新版本的WebLogic服務器會導致重新編譯所有JSP。正如描述過期檢查機制那一節所解釋的,在將JSP部署到不同版本的服務器時,JSP容器會重新編譯所有JSP。這麼做是爲了確保特定版本或服務包中的所有JSP編譯器/運行時增強或缺陷修復在生成的代碼中可用(沒有這一限制,可能會以一些類在JSP運行時引用不存在的方法而結束操作)。在理想情況下,預編譯JSP 的構建腳本必須使用與正在部署的服務器使用同一版本的weblogic.jar。建議將服務器使用的所有補丁和週期性修復添加到構建腳本使用的類路徑下。總之,構建和部署環境必須完全一致。這可以避免部署後遇到任何不必要重新編譯的問題。

進一步控制過期檢查
  在容器執行過期檢查時進行控制可以讓我們調優容器,使它運行得更好,因此爲JSP和servlet提供了更好的響應時間。每次過期檢查都要求JSP容器轉到磁盤併爲那個特定的JSP重新讀取最後修改時間。當調用太頻繁時,該過程可導致性能下降,因爲它影響JSP的響應時間。在理想情況下,需要過期檢查開發期間表現得非常活躍,特別是在應用程序經常改變時。通過單擊瀏覽器上的刷新/重載,並讓JSP容器重新編譯和重載新頁面,可很好地測試出對特殊JSP所做的修改。但在生產模式下,同樣的做法可能導致性能降低。
  以下參數的默認值最適合開發模式。建議你們在生產環境下進行部署時相應地修改這些值。

PageCheckSeconds
  對已經編譯好的JSP的每個新請求,容器都會從其配置文件(這裏是weblogic.xm)檢查pageCheckSeconds的值,如果上次過期檢查與當前時間之間的間隔大於pageCheckSeconds,那麼還要執行過期檢查操作。例如,假設pageCheckSeconds的值設爲10秒。對於針對foo.jsp的請求,容器執行檢查操作,以瞭解當前時間與最後過期檢查之間的間隔是否大於pageCheckSeconds。這裏假定頁面在10秒以前被重新編譯和訪問過;容器將檢查該類是否過期。在此間隔中,不會對foo.jsp的任何請求進行過期檢查。
  如果不是處於開發模式下,而且無需要每隔1秒(默認值)就檢查一次JSP頁面,那麼強烈推薦您將參數值更改爲-1(永遠不進行過期檢查)或者更改爲像60秒這樣的值。這避免了每次訪問JSP時都調用File.lastModified()、重載JSP類和檢查時間戳。

清單4. 來自weblogic.xml的代碼片段,展示了可如何設置該這個參數

<!DOCTYPE weblogic-web-app PUBLIC "-//BEA Systems, Inc.//DTD Web Application 8.1//EN"
 "http://www.bea.com/servers/wls810/dtd/weblogic810-web-jar.dtd">
<weblogic-web-app>
  <jsp-descriptor>
   <jsp-param>
     <param-name>pageCheckSeconds</param-name>
     <param-value>10</param-value>
   </jsp-param>
  </jsp-descriptor>
</weblogic-web-app>
  請注意,對於Web應用程序中的JSP從不個別改變的生產環境,最好配置容器永不對servlets和JSP執行過期檢查。

servlet-reload-check-secs
  在開發模式中,當servlet被修改和重新編譯到,比方說,迅速增長的WAR的WEB-INF/classes目錄中時,我們期望從瀏覽器執行請求時,容器調用它的最新版本的servlet。爲了處理該問題,WebLogic 的Web容器每隔servlet-reload-check-secs間隔就會檢查WEB-INF/classes中是否有文件被修改過。這個參數的默認值是1秒。對於希望看到對servlet類的最新修改但又不必重新部署應用程序的開發模式而言,這是一個很好的默認值。但在進入生產之前,必須將這個值更改爲-1(永不重載servlet文件)。在生產模式下,個別類不會改變,將servlet-reload-check-secs的值設爲-1總是最好的選擇。

清單5. servlet-reload-check-secs值設爲-1的weblogic.xml示例

<!DOCTYPE weblogic-web-app PUBLIC "-//BEA Systems, Inc.//DTD Web Application 8.1//EN"
 "http://www.bea.com/servers/wls810/dtd/weblogic810-web-jar.dtd">

<weblogic-web-app>
  <container-descriptor>
    <servlet-reload-check-secs>-1</servlet-reload-check-secs>
  </container-descriptor>
</weblogic-web-app>

JSP類加載器
  我們將通過查看WebLogic服務器如何加載JSP類來結束我們的討論。每個JSP都是在自己的類加載器(通常稱爲一次性類加載器)中加載的。該類加載器是Web應用程序類加載器的子加載器,負責加載有關的JSP類及其內部類(如果有的話)。好奇的讀者可能會覺得奇怪,爲什麼WebLogic在每個JSP自己的類加載器中加載JSP。真的需要這麼複雜嗎?WebLogic不能只用應用程序類加載器而使得生活更輕鬆嗎?所有這些問題都是有根據的,而且每個尋求WebLogic類加載器天堂的人都應該問自己這些問題。爲了解決這些問題,讓我們設想我們的Web應用程序有幾個JSP、少數servlet、一個過濾器以及幾百個還包含標籤處理器類的實用類。現在,假設所有這些類都被加載到單個類加載器中。如果修改單個JSP,然後單擊瀏覽器上的重載,那麼下面的事情將不得不發生:

 

  • JSP容器將不得不重新編譯頁面。
  • 將不得不丟棄用來加載較早類版本的整個Web應用程序類加載器。
  • 不得不創建一個新的Web應用程序類加載器,而且要重載和重新初始化所有servlet和JSP(包括剛纔修改的那個)。

  Java不允許重用類加載器來重載類的新版本。相反,您不得不放棄類加載器並創建一個新的。基於這個原因,上面的場景非常不合時宜;即使只更改了一個類,應用服務器也不得不重載大量的類。
  現在,讓我們看看WebLogic是如何實現其類加載器方案的。考慮一下前面提及的那個場景。如果我們修改JSP,並碰巧在瀏覽器上進行重載,那麼服務器將執行以下任務:

  • JSP容器重新編譯頁面。
  • 它丟棄用來加載這個JSP類的舊版本的那個JSP類加載器。
  • 它創建一個將Web應用程序類加載器作爲父加載器的新JSP類加載器,併爲該頁面提供服務。

  如您所見,只有一個JSP類必須重載,整個Web應用程序類加載器保持不動,並且不受我們對JSP所做小小改動的影響。因此,在修改單個JSP時,容器會丟棄舊的類加載器、重新編譯並只重載爲這個JSP生成的類。這避免了重載或拋棄整個Web應用程序類加載器。當只有某一特殊Web應用程序中的某些JSP經常改變時,這是一個很大的勝利。

結束語
  
有了這些關於JSP容器內部組織的知識,不但可以避免遭遇令人不快的、不必要的JSP重新編譯的狀況,而且還可以通過使用pageCheckSeconds和servlet-reload-checks-secs參數改進頁面響應時間自身。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章