名詞解釋:所謂熱部署,就是在應用正在運行的時候升級軟件,卻不須要從新啓動應用。java
對於Java應用程序來講,熱部署就是在運行時更新Java類文件。在基於Java的應用服務器實現熱部署的過程當中,類裝入器扮演着重要的角色
。大多數基於Java的應用服務器,包括EJB服務器和Servlet容器,都支持熱部署。類裝入器不能從新裝入一個已經裝入的類,但只要使用一個新的類裝入器實例,就能夠將類再次裝入一個正在運行的應用程序
。web
咱們知道,如今大多數的web服務器都支持熱部署,而對於熱部署的實現機制,網上講的卻不夠完善,下面咱們就Tomcat的熱部署實現機制,講解一下它是如何實現的:spring
Tomcat的容器實現熱部署使用了兩種機制:apache
Classloader重寫,經過自定義classloader加載相應的jsp編譯後的class到JVM中。tomcat
經過動態修改內存中的字節碼,將修改過的class再次裝載到JVM中。服務器
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來裝載它。app
關於如何使用自定義classloader來裝載一個class這裏就不說了,相信網上都能找到,JSP屬於一次性消費,每次調用容器將建立一個新的實例,屬於用完就扔的那種,可是對於這種實現方式卻很難用於其它狀況下,如如今咱們工程中不少都使用了單例,尤爲是spring工程,在這種狀況下使用新的classloader來加載修改後的類是不現實的,單例類將在內存中產生多個實例,並且這種方式沒法改變當前內存中已有實例的行爲,固然,tomcat也沒經過該方式實現class文件的從新加載。jsp
Tomcat中的class文件是經過org.apache.catalina.loader. WebappClassLoader
裝載的,一樣咱們能夠作個測試,測試過程與jsp測試相似,測試步驟就不說了,只說一下結果:ide
在熱部署的狀況下,對於被該classloader 加載的class文件,它的classloader始終是同一個WebappClassLoader
,除非容器重啓了,相信作完這個實驗你就不會再認爲tomcat是使用一個新的classloader來加載修改過的class了,並且對於有狀態的實例,以前該實例擁有的屬性和狀態都將保存,並在下次執行時擁有了新的class的邏輯,這就是熱部署的神祕之處(其實每一個實例只是保存了該實例的狀態屬性,咱們經過序列化對象就能看到對象中包含的狀態,最終的邏輯仍是存在於class文件中)。測試
下面的class重定義是經過:java.lang.instrument實現的
,具體可參考相關文檔。
下面咱們看一下如何經過代理修改內存中的class字節碼:
如下是一個簡單的熱部署代理實現類(代碼比較粗糙,也沒什麼判斷):
import java.lang.instrument.ClassFileTransformer; 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); } }
import java.lang.instrument.ClassFileTransformer; 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; } }
import java.lang.instrument.ClassDefinition; 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中的方法實現,將在不一樣時間看到不一樣的輸出結果,關於技術細節也沒什麼好講的了,相信你們都能明白。