tomcat熱部署的實現原理

atlantisholic發表於2011-07-19

一.             概述

名詞解釋:所謂熱部署,就是在應用正在執行的時候升級軟體,卻不需要重新啟動應用。

對於Java應用程式來說,熱部署就是在執行時更新Java類檔案。在基於Java的應用伺服器實現熱部署的過程中,類裝入器扮演著重要的角色。大多數基於Java的應用伺服器,包括EJB伺服器和Servlet容器,都支援熱部署。類裝入器不能重新裝入一個已經裝入的類,但只要使用一個新的類裝入器例項,就可以將類再次裝入一個正在執行的應用程式。

我們知道,現在大多數的web伺服器都支援熱部署,而對於熱部署的實現機制,網上講的卻不夠完善,下面我們就tomcat的熱部署實現機制,講解一下它是如何實現的:

Tomcat的容器實現熱部署使用了兩種機制:

1.  Classloader重寫,通過自定義classloader載入相應的jsp編譯後的classJVM中。

2.  通過動態修改記憶體中的位元組碼,將修改過的class再次裝載到JVM中。

 

二.             Classloader實現jsp的重新載入

Tomcat通過org.apache.jasper.servlet.JasperLoader實現了對jsp的載入,下面做個測試:

1. 新建一個web工程,並編寫一個jsp頁面,在jsp頁面中輸出該頁面的classloader,.

2.  啟動web伺服器,開啟jsp頁面,我們可以看到後臺輸出,該jspclassloaderJasperLoader的一個例項。

3.  修改jsp,儲存並重新整理jsp頁面,再次檢視後臺輸出,此classloader例項已經不是剛才那個了,也就是說tomcat通過一個新的classloader再次裝載了該jsp

4.  其實,對於每個jsp頁面tomcat都使用了一個獨立的classloader來裝載,每次修改完jsp後,tomcat都將使用一個新的classloader來裝載它。

 

關於如何使用自定義classloader來裝載一個class這裡就不說了,相信網上都能找到,JSP屬於一次性消費,每次呼叫容器將建立一個新的例項,屬於用完就扔的那種,但是對於這種實現方式卻很難用於其它情況下,如現在我們工程中很多都使用了單例,尤其是spring工程,在這種情況下使用新的classloader來載入修改後的類是不現實的,單例類將在記憶體中產生多個例項,而且這種方式無法改變當前記憶體中已有例項的行為,當然,tomcat也沒通過該方式實現class檔案的重新載入。

 

三.             通過代理修改記憶體中class的位元組碼

Tomcat中的class檔案是通過org.apache.catalina.loader. WebappClassLoader裝載的,同樣我們可以做個測試,測試過程與jsp測試類似,測試步驟就不說了,只說一下結果:

         在熱部署的情況下,對於被該classloader 載入的class檔案,它的classloader始終是同一個WebappClassLoader,除非容器重啟了,相信做完這個實驗你就不會再認為tomcat是使用一個新的classloader來載入修改過的class了,而且對於有狀態的例項,之前該例項擁有的屬性和狀態都將儲存,並在下次執行時擁有了新的class的邏輯,這就是熱部署的神祕之處(其實每個例項只是儲存了該例項的狀態屬性,我們通過序列化物件就能看到物件中包含的狀態,最終的邏輯還是存在於class檔案中)。

下面的class重定義是通過:java.lang.instrument實現的,具體可參考相關文件。

         下面我們看一下如何通過代理修改記憶體中的class位元組碼:

以下是一個簡單的熱部署代理實現類(程式碼比較粗糙,也沒什麼判斷):

package agent;

 

import java.lang.instrument.ClassFileTransformer;

import java.lang.instrument.Instrumentation;

import java.util.Set;

import java.util.Timer;

import java.util.TreeSet;

 

 

public  class  HotAgent {

    protected  static  Set  clsnames=new TreeSet();

   

    public  static  void  premain(String  agentArgs, Instrumentation  inst)  throws Exception {

       ClassFileTransformer  transformer =new ClassTransform(inst);

       inst.addTransformer(transformer);

       System.out.println("是否支援類的重定義:"+inst.isRedefineClassesSupported());

       Timer  timer=new  Timer();

      timer.schedule(new ReloadTask(inst),2000,2000);

    }

}

 

package agent;

 

import java.lang.instrument.ClassFileTransformer;

importjava.lang.instrument.IllegalClassFormatException;

import java.lang.instrument.Instrumentation;

import java.security.ProtectionDomain;

 

public  class  ClassTransform.  implements ClassFileTransformer {

    private  Instrumentation  inst;

    protected  ClassTransform(Instrumentation  inst){

       this.inst=inst;

    }

    /**

     * 此方法在redefineClasses時或者初次載入時會被呼叫,也就是說在class被再次載入時會被呼叫,

     * 並且我們通過此方法可以動態修改class位元組碼,實現類似代理之類的功能,具體方法可使用ASM或者javasist

     * 如果對位元組碼很熟悉的話可以直接修改位元組碼。

     */

    public  byte[]  transform(ClassLoader  loader, String  className,

           Class>  classBeingRedefined, ProtectionDomain  protectionDomain,

           byte[]  classfileBuffer)throws IllegalClassFormatException {

       byte[]  transformed = null;

       HotAgent.clsnames.add(className);

        return  null;

    }

}

 

package agent;

 

import java.io.InputStream;

import java.lang.instrument.ClassDefinition;

import java.lang.instrument.Instrumentation;

import java.util.TimerTask;

 

public  class  ReloadTask  extends  TimerTask {

   

    private  Instrumentation  inst;

    protected  ReloadTask(Instrumentation  inst){

       this.inst=inst;

    }

   

    @Override

    public  void  run() {

       try{

           ClassDefinition[]  cd=new ClassDefinition[1];

           Class[]  classes=inst.getAllLoadedClasses();

           for(Class  cls:classes){

          if(cls.getClassLoader()==null||!cls.getClassLoader().getClass().getName().equals("sun.misc.Launcher$AppClassLoader"))

                  continue;

              String  name=cls.getName().replaceAll("\\.","/");

             cd[0]=new ClassDefinition(cls,loadClassBytes(cls,name+".class"));

              inst.redefineClasses(cd);

           }

          

       }catch(Exception ex){

           ex.printStackTrace();

       }

    }

   

    private  byte[]  loadClassBytes(Class  cls,String  clsname) throws  Exception{

       System.out.println(clsname+":"+cls);

       InputStream  is=cls.getClassLoader().getSystemClassLoader().getResourceAsStream(clsname);

       if(is==null)return  null;

       byte[]  bt=new  byte[is.available()];

       is.read(bt);

       is.close();

       return  bt;

    }

}

 

以上是基本實現程式碼,需要元件為:

1.  HotAgent(預載入)

2.  ClassTransform(在載入class的時候可以修改class的位元組碼),本例中沒用到

3.  ReloadTaskclass定時載入器,以上程式碼僅供參考)

4.  META-INF/MANIFEST.MF內容為:(引數一:支援class重定義;引數二:預載入類)

Can-Redefine-Classes: true

Premain-Class: agent.HotAgent

5.  將以上元件打包成jar檔案(到此,元件已經完成,下面為編寫測試類檔案)。

6.  新建一個java工程,編寫一個java邏輯類,並編寫一個Test類,在該測試類中呼叫邏輯類的方法,下面看下測試類程式碼:

package test.redefine;

 

public  class  Bean1 {

    public  void  test1(){

      System.out.println("============================");

    }

}

 

package test.redefine;

 

public  class  Test {

    public  static  void  main(String[] args)throws  InterruptedException {

       Bean1  c1=new  Bean1();

       while(true){

           c1.test1();

           Thread.sleep(5000);

       }

    }

}

 

執行測試類:

java –javaagent:agent.jar test.redefine.Test

在測試類中,我們使用了一個死迴圈,定時呼叫邏輯類的方法。我們可以修改Bean1中的方法實現,將在不同時間看到不同的輸出結果,關於技術細節也沒什麼好講的了,相信大家都能明白。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/23071790/viewspace-702545/,如需轉載,請註明出處,否則將追究法律責任。

相關文章