JSP熱部署的實現原理

一.             概述

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

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

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

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

1.  Classloader重寫,通過自定義classloader加載相應的jsp編譯後的class到JVM中。

2.  通過動態修改內存中的字節碼,將修改過的class再次裝載到JVM中。

 

二.             Classloader實現jsp的重新加載

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

1. 新建一個web工程,並編寫一個jsp頁面,在jsp頁面中輸出該頁面的classloader,<%System.out.print(this.getClass().getClassLoader());%>.

2.  啓動web服務器,打開jsp頁面,我們可以看到後臺輸出,該jsp的classloader是JasperLoader的一個實例。

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<String>  clsnames=new TreeSet<String>();

    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.ReloadTask(class定時加載器,以上代碼僅供參考) 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中的方法實現,將在不同時間看到不同的輸出結果,關於技術細節也沒什麼好講的了,相信大家都能明白。

Tomcat 熱部署配置

<Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false">  
    <Context docBase="CPCWeb" path="/CPCWeb" reloadable="true" source="org.eclipse.jst.j2ee.server:CPCWeb"/>
</Host>   

autoDeploy="true" — 自動部署 reloadable="true" — 自動加載

原文地址:https://my.oschina.net/xianggao/blog/364068
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章