TWaver - 專注UI技術

          http://twaver.servasoft.com/
          posts - 171, comments - 191, trackbacks - 0, articles - 2
            BlogJava :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理

          Reload Class的陷阱

          Posted on 2010-08-23 13:28 TWaver 閱讀(1573) 評論(0)  編輯  收藏

          J2EE能在大規(guī)模項目中得到廣泛應用,我覺得主要有以下幾方面原因:

          1、JSP、Servlet的方式能夠很容易的將模塊進行劃分,而且模塊間很容易做到互不影響,幾個頁面有問題不會影響其它不相關的業(yè)務模塊運行,因此做到不同模塊不同時間上線的方式

          2、容器已經(jīng)處理好了很多基礎工作,一般程序員不用管socket通訊的并發(fā)、不用管如果使用線程池、不用管資源的同步死鎖問題、不用管數(shù)據(jù)庫連接池的實現(xiàn)、再加上Java的自動GC功能以及現(xiàn)在的硬件條件,即使不懂Java的程序員只要通過短期的項目經(jīng)歷,一般都能很快參與業(yè)務代碼的堆積(也許說這話有點侮辱程序員,不過不得不承認現(xiàn)在絕大部分公司拉的項目無非就是基于某個行業(yè)業(yè)務的增、刪、改、查+統(tǒng)計報表)

          3、web容器的reload功能大大提高了開發(fā)效率,修改了頁面或者業(yè)務類代碼不需要重新啟動JVM就能通過ClassLoader加載新的修改過得類進行測試,這點即使在上線運行時也起了很大的作用,由于現(xiàn)在我們的項目都是基于XMLHTTP的方式,在提交時用戶界面數(shù)據(jù)保持不變,如果提交出錯,打個電話過來改改后臺代碼,然后用戶再次點擊提交一切就OK了,這點相對于以前將業(yè)務代碼和UI界面綁定一塊,每次修改代碼得重新啟動JVM的CS程序是不可思議的方便

          回到今天的話題,以上的第3點可是存在陷阱的地方,以下論述是基于WebLogic的測試結果:

          當我們修改某個Java類保存編譯后你會發(fā)現(xiàn)console輸出控制臺似乎什么都沒發(fā)生,其實如果你在每個類加上 static{ System.out.println(“loading XXX.class”); },你會發(fā)現(xiàn)任何類的修改將會導致所有(甚至與改修改類沒有任何瓜葛的類)應用類被重新加載,當然weblogic是用lazy load的方式,你調(diào)用到那個類就重新加載哪個類。這樣如果系統(tǒng)中緩存的數(shù)據(jù)以及已經(jīng)設置為某種狀態(tài)的static靜態(tài)屬性將會被重新初始化,這樣就很可能破壞某些正常的業(yè)務邏輯,出現(xiàn)奇怪的讓人覺得不可能發(fā)生的問題:“我明明初始化了這個實例了…,我明明通過跟蹤斷點發(fā)現(xiàn)某某屬性已經(jīng)被我設置為…,怎么現(xiàn)在又成了….”。

          還有一種出錯的情況,對于需要定時任務的系統(tǒng)(例如簡單的java.util.Timer或者功能強大的開源quartz)也許你會實現(xiàn)為類似如下方式:

           1public class Task extends TimerTask{
           2    public static Timer timer = new Timer();
           3    static{
           4        System.out.println("loading Task.class " + timer);
           5    }

           6    public void run() {
           7        System.out.println("i am doing at " + new Date());
           8    }

           9    public static void start(){
          10        Task.timer.schedule(new Task(), 500010000);
          11    }

          12    public static void stop(){
          13        Task.timer.cancel();
          14    }

          15}
           
          16
          17public class SchedulerManager
          18 implements  ServletContextListener{
          19    public void contextInitialized(ServletContextEvent arg0) {
          20        Task.start();
          21    }

          22    public void contextDestroyed(ServletContextEvent arg0) {
          23        Task.stop();
          24    }

          25}

          1<listener>
          2    <listener-class>test.SchedulerManager</listener-class>
          3</listener>

          通過上下文監(jiān)聽啟動和關閉定時器,正常運行是沒有問題的,但是如果你修改了類導致容器重新加載class那么問題出現(xiàn)了,例如你可以試著將 System.out.println(“i am doing at ” + new Date());
          簡單修改為 System.out.println(“i am new doing at ” + new Date());接著隨便找個jsp運行這樣的代碼:

          System.out.println(“=========================================”);
          (new Task()).run();

          你會發(fā)現(xiàn)在輸出日志種將出現(xiàn)“新老類共存”的效果:

          i am doing at Sat Jun 25 22:45:27 CST 2005
          i am doing at Sat Jun 25 22:45:37 CST 2005
          i am doing at Sat Jun 25 22:46:01 CST 2005
          =========================================
          i am new doing at Sat Jun 25 22:46:02 CST 2005
          i am doing at Sat Jun 25 22:46:11 CST 2005
          i am doing at Sat Jun 25 22:46:21 CST 2005
          i am doing at Sat Jun 25 22:46:31 CST 2005

          而且這時如果你想通過Task.stop();停止定時器,對不起,那個第一次被啟動的Task已經(jīng)是“另一個空間”的東東了,你是觸及不到的了,如果你再調(diào)用Task.start()那系統(tǒng)將有兩個Timer在跑一個跑著老的類,一個跑著新的類,你可以調(diào)用Task.stop();關閉新的Timer但是老的Timer只有redeploy或者重啟整個JVM才能關閉了,終其原因是重新加載類和重新部署web工程效果是不一樣的,reload class并不調(diào)用監(jiān)聽器的contextDestroyed和contextInitialized函數(shù),所以這樣情況下用Servlet是個不錯的選擇:

          1public class TestServlet extends HttpServlet {
          2    public void init() throws ServletException {
          3        Task.start();
          4    }

          5    public void destroy() {
          6        Task.stop();
          7    }

          8}


          1<servlet>
          2  <servlet-name>TestServlet</servlet-name>
          3  <servlet-class>test.TestServlet</servlet-class>
          4  <load-on-startup>1</load-on-startup>
          5</servlet>

          如果你修改了Task類,而且Task被調(diào)用到并且reload了,但是TestServlet還沒被調(diào)用到,所以還沒reload TestServlet那么也是會出現(xiàn)新老類并存的現(xiàn)象,但是一旦TestServlet被觸及那么在reload之前destroy將會被調(diào)用,接著init將會被調(diào)用,這樣系統(tǒng)將會恢復正常狀態(tài),老類不再運行,只有新類在運行,用Servlet的方式至少不會出現(xiàn)老類永遠觸及不到無非關閉的情況。

                 按照這種說法Servlet似乎可以解決所有問題了,其實非也,如果你只用Servlet的“正統(tǒng)”用法操作不會有任何問題,但是如果你在某些情況下需要不通過Servlet的方式而是用普通類的方式直接操作Servlet的函數(shù)那么問題就來了。舉個例子:我們利用Servlet在init()時啟動quartz任務定制器,但是在系統(tǒng)運行中我們需要添加新的任務和修改任務參數(shù),簡單的方式就是全部重新加載所有任務,為此你也許回在servlet中提供public static void reload()的函數(shù),這就是問題的根源了,例如在調(diào)用reload之前有其他的類(或者就是這個Servlet本身)被修改過了,當你調(diào)用reload函數(shù)時這個Servlet需要reload class,但是由于你并非通過“正統(tǒng)”的Servlet訪問方式操作,而是用類的普通調(diào)用方式操作的,所以weblogic容器(其他容器我還沒測試過,也許會有不一樣的效果)在reload class時不再調(diào)用Servlet的public void destroy()函數(shù),并且在reload class是也不再調(diào)用Servlet的public void init()函數(shù),這樣的話以前啟動的org.quartz.Scheduler實例再也沒有機會調(diào)用scheduler.shutdown(false)進行關閉了。

          以下代碼是個不優(yōu)雅但是管用的方法:

           1<IMG style="display:none" id="SchedulerServlet" BORDER="1" WIDTH=0 HEIGHT=0/>
           2
           3<SCRIPT LANGUAGE="JavaScript">
           4<!--
           5function reload(){
           6    document.all("SchedulerServlet").src = "/servlet/SchedulerServlet";
           7    if(window.confirm("你確信要重新加載作業(yè)信息?")){
           8        ×××  通過XMLHTTP遠程調(diào)用SchedulerServlet.reload() ×××
           9    }

          10}

          11//-->
          12</SCRIPT>

          這樣在“非正統(tǒng)”的SchedulerServlet.reload()調(diào)用之間已經(jīng)有個src = “/servlet/SchedulerServlet”的“正統(tǒng)”調(diào)用被除觸發(fā)了。不過以上解決方法純粹脫褲放屁,直接通過HTTP請求servlet的get、post函數(shù)進行處理不就得了,所以以下的方案才是正道:

           1function reload(){
           2    if(window.confirm("你確信要重新加載作業(yè)信息?")) {
           3        document.all("SchedulerManager").src = "/SchedulerManager?action=reload";
           4    }

           5}

           6
           7function shutdown(){
           8    if(window.confirm("你確信要定制所有作業(yè)?")){
           9        document.all("SchedulerManager").src = "/SchedulerManager?action=shutdown";
          10    }

          11}

          最后讓我們來研究一下reload class對容器中HTTP會話session的影響,我們在開發(fā)時修改類并不需要重新登錄就能進行測試,說明reload class時session還保存著用戶數(shù)據(jù),那么這些數(shù)據(jù)是完整的沒問題的嗎?眼見為實先讓我們做幾個測試:

           1// 不可串行化的普通NoSerial類
           2public class NoSerial {
           3   static{
           4      System.out.println("loading NoSerial.class");
           5   }

           6   public NoSerial(){
           7      System.out.println("NoSerial構造中");
           8   }

           9   public int prop = 1;
          10}

          11
          12// 實現(xiàn)Serializable接口的可串行化Data類,writeObject與readObject可以不實現(xiàn)
          13// 對這兩個函數(shù)的實現(xiàn)只是簡單的調(diào)用了默認的操作,為了輸出日志方便監(jiān)控過程
          14public class Data implements Serializable {
          15   static{
          16      System.out.println("loading Data.class");
          17   }

          18   public Data(){
          19      System.out.println("Data構造中");
          20   }

          21   public int prop = 1;
          22   private void writeObject(ObjectOutputStream stream) throws IOException {
          23      System.out.println("writeObject");
          24      stream.defaultWriteObject();
          25   }

          26    private void readObject(ObjectInputStream stream) throws IOException,  ClassNotFoundException {
          27      System.out.println("readObject");
          28      stream.defaultReadObject();
          29   }

          30}

          31
          32// 實現(xiàn)Externalizable接口的可串行化ExterData類
          33public class ExterData implements Externalizable{
          34   static{
          35      System.out.println("loading ExterData.class");
          36   }

          37   public ExterData(){
          38      System.out.println("ExterData構造中");
          39   }

          40   public int prop = 1;
          41   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
          42      System.out.println("readExternal");
          43   prop = in.readInt();
          44   }

          45   public void writeExternal(ObjectOutput out) throws IOException {
          46      System.out.println("writeExternal");
          47      out.writeInt(prop);
          48   }

          49}

          50
          51////////////////    在session中存入一下三個類的實例   //////////////////
          52System.out.println("【##############################】");
          53session.setAttribute("NoSerial"new test.NoSerial());
          54Object noSerial = session.getAttribute("NoSerial");
          55System.out.println((noSerial instanceof NoSerial) + " | " + noSerial);
          56System.out.println("【------------------------------】");
          57session.setAttribute("data"new test.Data());
          58Object data = session.getAttribute("data");
          59System.out.println((data instanceof Data) + " | " + data);
          60System.out.println("【******************************】");
          61session.setAttribute("ExterData"new test.ExterData());
          62Object exterData = session.getAttribute("ExterData");
          63System.out.println((exterData instanceof ExterData) + " | " + exterData);

          輸出日志:
          【##############################】
          NoSerial構造中…
          true | test.NoSerial@a2dbe8
          【——————————】
          Data構造中…
          true | test.Data@138ce2
          【******************************】
          ExterData構造中…
          true | test.ExterData@18654c0

           1// 屏蔽掉session.setAttribute的代碼,并且隨便修改一下其他的類,觸發(fā)一下容器的reload class //
           2System.out.println("【##############################】");
           3//session.setAttribute("NoSerial", new test.NoSerial());
           4Object noSerial = session.getAttribute("NoSerial");
           5System.out.println((noSerial instanceof NoSerial) + " | " + noSerial);
           6System.out.println("【------------------------------】");
           7//session.setAttribute("data", new test.Data());
           8Object data = session.getAttribute("data");
           9System.out.println((data instanceof Data) + " | " + data);
          10System.out.println("【******************************】");
          11//session.setAttribute("ExterData", new test.ExterData());
          12Object exterData = session.getAttribute("ExterData");
          13System.out.println((exterData instanceof ExterData) + " | " + exterData);

          輸出日志:
          【##############################】
          false | test.NoSerial@6b3836
          【——————————】
          writeObject…
          loading Data.class…
          readObject…
          true | test.Data@12a14a6
          【******************************】
          writeExternal…
          loading ExterData.class…
          ExterData構造中…
          readExternal…
          true | test.ExterData@14ea478

          通過以上的測試可知reload class對于容器session中的內(nèi)容的影響:

          對于非可串行話的實例NoSerial容器不進行任何操作,這樣這個NoSerial實例仍然為以前的classLoad加載進來的class創(chuàng)建的實例,與現(xiàn)在重新加載的class已經(jīng)沒有關系了,所以進行instanceof的判斷時結果為false,因此如果你想NoSerial noSerial = (NoSerial)session.getAttribute(“NoSerial”);這樣獲取該實例的話將會拋出類ClassCastException異常

          對于實現(xiàn)了Serializable或者Externalizable接口的串行化類容器在重新加載類前將會先串行化(writeObject和writeExternal)保存實例內(nèi)容,然后加載類loading XXX.class,最后重新初始化實例(這里有點小細節(jié),對于Serializable沒有調(diào)用不帶參數(shù)的構造函數(shù),對于Externalizable進行調(diào)用了,所以有l(wèi)oading ExterData.class…日志的輸出)調(diào)用readObject和readExternal恢復原來的數(shù)據(jù),因此進行instanceof的判斷時結果為true,可以安全的取出來操作。所以對于需要保存再session中的類最好實現(xiàn)為可串行化的,這不僅有利于容器在內(nèi)存受限時將會話內(nèi)容保存到磁盤避免outofmemory的危險,而且在reload class后還可以正常操作session中的對象內(nèi)容。

          對于實現(xiàn)串行化有有幾點需要說明一下, 如果你不設置private static final long serialVersionUID = XXXL;屬性,那么當你改動類的屬性、函數(shù)名(函數(shù)的實現(xiàn)修改不要緊,JVM計算serialVersionUID的哈西算法并不考慮函數(shù)內(nèi)容,所以修改函數(shù)內(nèi)容并不會影響serialVersionUID值)等信息時會造成JVM計算的serialVersionUID前后不一致,會出現(xiàn)java.io.InvalidClassException: zt.cims.utils.test.Data; local class incompatible: stream classdesc serialVersionUID = -8934056548762896729, local class serialVersionUID = -5363502546919843732之類的異常。如果你設置了serialVersionUID屬性那么默認的串行化讀取時會運用“最小化損失”原則進行屬性匹配進行賦值,也就是說如果以前有i、j屬性,現(xiàn)在添加了k屬性那么i、j屬性將會被填充而k不進行處理,至于函數(shù)部分你可以任意改動甚至是函數(shù)名和參數(shù)。

          最后對于序列化方面TWaver Java的TWaverUtil上有幾個函數(shù)挺方便的,也許你用得上

          1public static byte[] toBytes(Object object)
          2public static Object toObject(byte[] bytes)
          3public static byte[] toByteByGZIP(Object object)
          4public static Object toObjectByGZIP(byte[] bytes)

          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導航:
           
          主站蜘蛛池模板: 南召县| 镇江市| 林周县| 三明市| 高邑县| 莱西市| 天镇县| 色达县| 依兰县| 瑞昌市| 蒲江县| 凤庆县| 三台县| 九龙县| 五华县| 宁明县| 鹿邑县| 会昌县| 凤城市| 凤山市| 龙州县| 鄄城县| 龙胜| 定襄县| 保靖县| 高州市| 邯郸市| 息烽县| 滨海县| 长宁县| 游戏| 汝州市| 酒泉市| 于都县| 剑阁县| 运城市| 平潭县| 古田县| 沅陵县| 秭归县| 乡城县|