熱加載和熱部署,沒聽過?看看 Tomcat 是怎么實(shí)現(xiàn)的(tomcat熱加載原理)
熱部署就是在服務(wù)器運(yùn)行時(shí)重新部署項(xiàng)目,熱加載即在在運(yùn)行時(shí)重新加載class,從而升級(jí)應(yīng)用。
通常情況下在開發(fā)環(huán)境中我們使用的是熱加載,因?yàn)闊峒虞d的實(shí)現(xiàn)的方式在Web容器中啟動(dòng)一個(gè)后臺(tái)線程,定期檢測(cè)相關(guān)文件的變化,如果有變化就重新加載類,這個(gè)過程不會(huì)清空Session。而在生產(chǎn)環(huán)境我們一般應(yīng)用的是熱部署,熱部署也是在Web應(yīng)用后臺(tái)線程定期檢測(cè),發(fā)現(xiàn)有變化就會(huì)重新加載整個(gè)Web應(yīng)用,這種方式更加徹底會(huì)清空Session。
熱加載
熱加載其實(shí)我們?cè)陂_發(fā)過程中經(jīng)常使用,例如我們使用Idea開發(fā)時(shí),我們?cè)谠O(shè)置頁面可以進(jìn)行設(shè)置,當(dāng)修改文件時(shí),我們可以選擇不重啟項(xiàng)目,選擇重新加載此文件。而在Tomcat中也能設(shè)置,Tomcat默認(rèn)情況下是不開啟熱加載的。需要在Tomcat路徑下的Context.xml中配置reloadable參數(shù)來開啟這個(gè)功能。
<Context reloadable="true"/>復(fù)制代碼
我們演示一下Tomcat是如何熱加載的。在webapp下我們新建了一個(gè)項(xiàng)目,里面的Servlet文件如下
public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("MyServlet 在處理 get()請(qǐng)求..."); PrintWriter out = response.getWriter(); response.setContentType("text/html;charset=utf-8"); out.println("<strong>>My Servlet Version1!</strong><br>"); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("MyServlet 在處理 post()請(qǐng)求..."); PrintWriter out = response.getWriter(); response.setContentType("text/html;charset=utf-8"); out.println("<strong>>My Servlet Version1!</strong><br>"); }}復(fù)制代碼
目錄結(jié)構(gòu)如下
-webapp -- mywebapp -- WEB-INF -- web.xml -- classes -- MyServlet.class復(fù)制代碼
為了演示Tomcat運(yùn)行時(shí)能修改class文件能夠動(dòng)態(tài)加載。我們分為以下三步
- 正常啟動(dòng)Tomcat。輸入http://localhost:8080/mywebapp/myservlet,觀察頁面輸出
- 在Tomcat啟動(dòng)的情況下修改MyServlet文件后覆蓋原來的class文件
- 再次觀察頁面情況。觀察頁面輸出是否修改
下面直接用動(dòng)態(tài)圖演示效果,更直觀一些。
我們可以看到在Tomcat運(yùn)行的情況下,直接替換class文件是能夠直接生效的。那么Tomcat是如何做到的呢?其實(shí)我們可以自己推導(dǎo)一下。
- 所有的class文件都是交由類加載來管理的
- 如果換了class文件是不是只需要更換相應(yīng)的類加載器重新加載就行
那么接下來我們來驗(yàn)證我們的結(jié)論,看一下在Tomcat中是如何實(shí)現(xiàn)熱加載的。Tomcat要監(jiān)聽class文件是否變化應(yīng)該是新起了一個(gè)線程來觀測(cè)。那么看到在Context的啟動(dòng)方法中,看到調(diào)用了threadStart的方法。
protected void threadStart() { backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor() .scheduleWithFixedDelay(new ContainerBackgroundProcessor(),//要執(zhí)行的Runnable backgroundProcessorDelay, //第一次執(zhí)行延遲多久 backgroundProcessorDelay, //之后每次隔多久執(zhí)行一次 TimeUnit.SECONDS); //時(shí)間單位 }}復(fù)制代碼
其中在后臺(tái)開啟周期性的任務(wù),使用了Java提供的ScheduledThreadPoolExecutor。除了能周期性執(zhí)行任務(wù)以外還有線程池的功能。上面代碼中調(diào)用了scheduleWithFixedDelay方法,第一個(gè)傳入的參數(shù)就是要執(zhí)行的任務(wù)。我們接下來看任務(wù)類ContainerBackgroundProcessor是如何實(shí)現(xiàn)的。
protected class ContainerBackgroundProcessor implements Runnable { @Override public void run() { // 請(qǐng)注意這里傳入的參數(shù)是 " 宿主類 " 的實(shí)例 processChildren(ContainerBase.this); } protected void processChildren(Container container) { try { //1. 調(diào)用當(dāng)前容器的 backgroundProcess 方法。 container.backgroundProcess(); //2. 遍歷所有的子容器,遞歸調(diào)用 processChildren, // 這樣當(dāng)前容器的子孫都會(huì)被處理 Container[] children = container.findChildren(); for (int i = 0; i < children.length; i ) { // 這里會(huì)判斷子容器如果已經(jīng)啟動(dòng)了后臺(tái)線程,那么這里就不會(huì)啟動(dòng)了 if (children[i].getBackgroundProcessorDelay() <= 0) { processChildren(children[i]); } } } catch (Throwable t) { ... }復(fù)制代碼
上面代碼中我們可以知道具體的后臺(tái)監(jiān)聽代碼是在backgroundProcess方法中實(shí)現(xiàn)的。那么我們看Context容器的backgroundProcess方法是如何實(shí)現(xiàn)的。
public void backgroundProcess() { //WebappLoader 周期性的檢查 WEB-INF/classes 和 WEB-INF/lib 目錄下的類文件 Loader loader = getLoader(); if (loader != null) { loader.backgroundProcess(); } ............省略}
進(jìn)去loader.backgroundProcess();中我們可以看到
public void backgroundProcess() { //此處判斷熱加載開關(guān)是否開啟和監(jiān)控的文件夾中文件是否有修改 if (reloadable && modified()) { try { Thread.currentThread().setContextClassLoader (WebappLoader.class.getClassLoader()); if (context != null) { //Context重啟 context.reload(); } } finally { if (context != null && context.getLoader() != null) { Thread.currentThread().setContextClassLoader (context.getLoader().getClassLoader()); } } }}
我們可以發(fā)現(xiàn)Tomcat熱加載的步驟
- 如果發(fā)現(xiàn)有文件發(fā)生變化,熱加載開關(guān)開啟
- 關(guān)閉Context容器
- 重啟Context容器
在這個(gè)過程中,最重要的部分其實(shí)就是類加載器了。因?yàn)橐粋€(gè)Context容器對(duì)應(yīng)一個(gè)類加載器。所以在銷毀Context容器的時(shí)候也連帶著將其類加載器一并銷毀了。Context在重啟的過程中也會(huì)創(chuàng)建新的類加載器來加載我們新建的文件。
熱部署
如果還是不懂熱部署是什么的,下面演示一遍應(yīng)該就明白了。Tomcat在啟動(dòng)的時(shí)候會(huì)將其目錄下webapp中war包解壓后然后封裝為一個(gè)Context供外部訪問。那么熱部署就是在程序運(yùn)行時(shí),如果我們修改了War包中的東西。那么Tomcat就會(huì)刪除之前的War包解壓的文件夾,重新解壓新的War包。
我們發(fā)現(xiàn)上面動(dòng)圖中在Tomcat運(yùn)行時(shí),我們修改了War包的信息,它就會(huì)將原來的刪除然后重新生成一份。
我們從上面的動(dòng)圖中其實(shí)就看出了熱部署和熱加載的區(qū)別了。熱部署是將文件夾刪除然后重新解壓包。那么熱加載是由Context容器負(fù)責(zé)的。那么熱部署又是由哪個(gè)容器負(fù)責(zé)呢?因?yàn)橐粋€(gè)文件夾對(duì)應(yīng)一個(gè)Context。既然文件夾都刪除了,那么肯定不是由Context容器負(fù)責(zé)了。那么應(yīng)該就是Context的父容器Host來負(fù)責(zé)。
我們可以看到Host容器并沒有實(shí)現(xiàn)自己的backgroundProcess方法。那么它是如何監(jiān)聽的呢?既然它沒有實(shí)現(xiàn)方法,肯定是調(diào)用了父類的backgroundProcess方法。我們可以看到在父類的backgroundProcess中
@Override public void backgroundProcess() { . ...........省略 fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null); }
可以看到周期事件的監(jiān)聽器。而Host的事件監(jiān)聽器是HostConfig類的lifecycleEvent方法
@Override public void lifecycleEvent(LifecycleEvent event) { if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {// 周期事件 check(); } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {// 開始之前事件 beforeStart(); } else if (event.getType().equals(Lifecycle.START_EVENT)) { // 開始事件 start(); } else if (event.getType().equals(Lifecycle.STOP_EVENT)) { // 結(jié)束事件 stop(); } }
我們可以看check方法
protected void check() { if (host.getAutoDeploy()) { // 檢查Host下所有已經(jīng)部署的web應(yīng)用 DeployedApplication[] apps = deployed.values().toArray(new DeployedApplication[0]); for (int i = 0; i < apps.length; i ) { if (!isServiced(apps[i].name)) checkResources(apps[i], false); } // 檢查Web應(yīng)用是否有變化 if (host.getUndeployOldVersions()) { checkUndeploy(); } // 執(zhí)行部署 deployApps(); }}復(fù)制代碼
熱部署的步驟其實(shí)也可以簡(jiǎn)化為三步驟
- 檢查Host管理下的所有web應(yīng)用
- 如果原來的Web應(yīng)用被刪除,就將相應(yīng)Context容器刪除
- 如果有新War包放進(jìn)來,就部署相應(yīng)的War包
作者 | 不學(xué)無數(shù)的程序員
來源 | https://urlify.cn/Ir6Z7n