死磕Tomcat系列(6)——Tomcat如何作到熱加載和熱部署的

死磕Tomcat系列(6)——Tomcat如何作到熱加載和熱部署的

熱部署就是在服務器運行時從新部署項目,熱加載即在在運行時從新加載class,從而升級應用。html

一般狀況下在開發環境中咱們使用的是熱加載,由於熱加載的實現的方式在Web容器中啓動一個後臺線程,按期檢測相關文件的變化,若是有變化就從新加載類,這個過程不會清空Session。而在生產環境咱們通常應用的是熱部署,熱部署也是在Web應用後臺線程按期檢測,發現有變化就會從新加載整個Web應用,這種方式更加完全會清空Session。web

熱加載

熱加載其實咱們在開發過程當中常常使用,例如咱們使用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>");
    }

}

目錄結構以下app

-webapp
	-- mywebapp
		-- WEB-INF
			-- web.xml
			-- classes
				-- MyServlet.class

爲了演示Tomcat運行時能修改class文件可以動態加載。咱們分爲如下三步webapp

  1. 正常啓動Tomcat。輸入http://localhost:8080/mywebapp/myservlet,觀察頁面輸出
  2. 在Tomcat啓動的狀況下修改MyServlet文件後覆蓋原來的class文件
  3. 再次觀察頁面狀況。觀察頁面輸出是否修改

下面直接用動態圖演示效果,更直觀一些。ide

咱們能夠看到在Tomcat運行的狀況下,直接替換class文件是可以直接生效的。那麼Tomcat是如何作到的呢?其實咱們能夠本身推導一下。post

  1. 全部的class文件都是交由類加載來管理的
  2. 若是換了class文件是否是隻須要更換相應的類加載器從新加載就行

那麼接下來咱們來驗證咱們的結論,看一下在Tomcat中是如何實現熱加載的。Tomcat要監聽class文件是否變化應該是新起了一個線程來觀測。那麼看到在Context的啓動方法中,看到調用了threadStart的方法。this

protected void threadStart() {
        backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
                .scheduleWithFixedDelay(new ContainerBackgroundProcessor(),//要執行的Runnable
                        backgroundProcessorDelay, //第一次執行延遲多久
                        backgroundProcessorDelay, //以後每次隔多久執行一次
                        TimeUnit.SECONDS); //時間單位
    }
}

其中在後臺開啓週期性的任務,使用了Java提供的ScheduledThreadPoolExecutor。除了能週期性執行任務之外還有線程池的功能。上面代碼中調用了scheduleWithFixedDelay 方法,第一個傳入的參數就是要執行的任務。咱們接下來看任務類ContainerBackgroundProcessor 是如何實現的。.net

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熱加載的步驟

  1. 若是發現有文件發生變化,熱加載開關開啓
  2. 關閉Context容器
  3. 重啓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();
    }
}

熱部署的步驟其實也能夠簡化爲三步驟

  1. 檢查Host管理下的全部web應用
  2. 若是原來的Web應用被刪除,就將相應Context容器刪除
  3. 若是有新War包放進來,就部署相應的War包

參考文章

往期文章

相關文章
相關標籤/搜索