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