Java伺服器熱部署的實現原理

ldear發表於2017-08-31

今天發現早年在大象筆記中寫的一篇筆記,之前放在ijavaboy上的,現在它已經訪問不了了。前幾天又有同事在討論這個問題。這裡拿來分享一下。


在web應用開發或者遊戲伺服器開發的過程中,我們時時刻刻都在使用熱部署。熱部署的目的很簡單,就是為了節省應用開發和釋出的時間。比如,我們在使用Tomcat或者Jboss等應用伺服器開發應用時,我們經常會開啟熱部署功能。熱部署,簡單點來說,就是我們將打包好的應用直接替換掉原有的應用,不用關閉或者重啟伺服器,一切就是這麼簡單。那麼,熱部署到底是如何實現的呢?在本文中,我將寫一個例項,這個例項就是一個容器應用,允許使用者釋出自己的應用,同時支援熱部署。


在Java中,要實現熱部署,首先,你得明白,Java中類的載入方式。每一個應用程式的類都會被ClassLoader載入,所以,要實現一個支援熱部署的應用,我們可以對每一個使用者自定義的應用程式使用一個單獨的ClassLoader進行載入。然後,當某個使用者自定義的應用程式發生變化的時候,我們首先銷燬原來的應用,然後使用一個新的ClassLoader來載入改變之後的應用。而所有其他的應用程式不會受到一點干擾。先看一下,該應用的設計圖:



有了總體實現思路之後,我們可以想到如下幾個需要完成的目標:

1、定義一個使用者自定義應用程式的介面,這是因為,我們需要在容器應用中去載入使用者自定義的應用程式。
2、我們還需要一個配置檔案,讓使用者去配置他們的應用程式。
3、應用啟動的時候,載入所有已有的使用者自定義應用程式。
4、為了支援熱部署,我們需要一個監聽器,來監聽應用釋出目錄中每個檔案的變動。這樣,當某個應用重新部署之後,我們就可以得到通知,進而進行熱部署處理。

實現部分:

首先,我們定義一個介面,每一個使用者自定義的程式中都必須包含唯一一個實現了該介面的類。程式碼如下:
  1. public interface IApplication {  
  2.   
  3.         public void init();  
  4.          
  5.         public void execute();  
  6.          
  7.         public void destory();  
  8.          
  9. }  


在這個例子中,每一個使用者自定義的應用程式,都必須首先打包成一個jar檔案,然後釋出到一個指定的目錄,按照指定的格式,然後首次釋出的時候,還需要將應用的配置新增到配置檔案中。所以,首先,我們需要定義一個可以載入指定目錄jar檔案的類:
  1.  public ClassLoader createClassLoader(ClassLoader parentClassLoader, String... folders) {  
  2.   
  3.        List<URL> jarsToLoad = new ArrayList<URL>();  
  4.         for (String folder : folders) {  
  5.               List<String> jarPaths = scanJarFiles(folder);  
  6.   
  7.                for (String jar : jarPaths) {  
  8.   
  9.                      try {  
  10.                            File file = new File(jar);  
  11.                            jarsToLoad.add(file.toURI().toURL());  
  12.   
  13.                     } catch (MalformedURLException e) {  
  14.                            e.printStackTrace();  
  15.                     }  
  16.               }  
  17.        }  
  18.   
  19.        URL[] urls = new URL[jarsToLoad.size()];  
  20.        jarsToLoad.toArray(urls);  
  21.   
  22.         return new URLClassLoader(urls, parentClassLoader);  
  23. }  



這個方法很簡單,就是從多個目錄中掃描jar檔案,然後返回一個新的URLClassLoader例項。至於scanJarFiles方法,你可以隨後下載本文的原始碼。然後,我們需要定義一個配置檔案,使用者需要將他們自定義的應用程式資訊配置在這裡,這樣,該容器應用隨後就根據這個配置檔案來載入所有的應用程式:
  1. <apps>  
  2.         <app>  
  3.                <name> TestApplication1</name >  
  4.                <file> com.ijavaboy.app.TestApplication1</file >  
  5.         </app>  
  6.         <app>  
  7.                <name> TestApplication2</name >  
  8.                <file> com.ijavaboy.app.TestApplication2</file >  
  9.         </app>  
  10. </apps>  


這個配置是XML格式的,每一個app標籤就表示一個應用程式,每一個應用程式,需要配置名稱和那個實現了IApplication介面的類的完整路徑和名稱。
有了這個配置檔案,我們需要對其進行解析,在這個例子中,我使用的是xstream,很簡單,你可以下載原始碼,然後看看就知道了。這裡略過。這裡需要提一下:每個應用的名稱(name),是至關重要的,因為該例子中,我們的釋出目錄是整個專案釋出目錄下的applications目錄,這是所有使用者自定義應用程式釋出的目錄。而使用者釋出一個應用程式,需要首先在該目錄下新建一個和這裡配置的name一樣名稱的資料夾,然後將打包好的應用釋出到該資料夾中。(你必須這樣做,否則在這個例子中,你會發布失敗)。
好了,現在載入jar的方法和配置都有了,下面將是整個例子的核心部分,對,就是應用程式管理類,這個類就是要完成對每一個使用者自定義應用程式的管理和維護。首先要做的,就是如何載入一個應用程式:

  1. public void createApplication(String basePath, AppConfig config){  
  2.       String folderName = basePath + GlobalSetting. JAR_FOLDER + config.getName();  
  3.       ClassLoader loader = this.jarLoader .createClassLoader(ApplicationManager. class.getClassLoader(), folderName);  
  4.         
  5.        try {  
  6.              Class<?> appClass = loader. loadClass(config.getFile());  
  7.                
  8.              IApplication app = (IApplication)appClass.newInstance();  
  9.                
  10.              app.init();  
  11.                
  12.               this.apps .put(config.getName(), app);  
  13.                
  14.       } catch (ClassNotFoundException e) {  
  15.              e.printStackTrace();  
  16.       } catch (InstantiationException e) {  
  17.              e.printStackTrace();  
  18.       } catch (IllegalAccessException e) {  
  19.              e.printStackTrace();  
  20.       }  


可以看到,這個方法接收兩個引數,一個是基本路徑,一個是應用程式配置。基本路徑其實就是專案釋出目錄的地址,而AppConfig其實就是配置檔案中app標籤的一個實體對映,這個方法從指定的配置目錄中載入指定的類,然後呼叫該應用的init方法,完成使用者自定義應用程式的初始化。最後將,該載入的應用放入記憶體中。
現在,所有的準備工作,都已經完成了。接下來,在整個應用程式啟動的時候,我們需要載入所有的使用者自定義應用程式,所以,我們在ApplicationManager中新增一個方法:

  1.  public void loadAllApplications(String basePath){  
  2.          
  3.         for(AppConfig config : this.configManager.getConfigs()){  
  4.                this.createApplication(basePath, config);  
  5.        }  
  6. }  


這個方法,就是將使用者配置的所有應用程式載入到該容器應用中來。好了,現在我們是不是需要寫兩個獨立的應用程式試試效果了,要寫這個應用程式,首先我們新建一個java應用程式,然後引用這個例子專案,或者將該例子專案打包成一個jar檔案,然後引用到這個獨立的應用中來,因為這個獨立的應用程式中,必須要包含一個實現了IApplication介面的類。我們來看看這個例子包含的一個獨立應用的樣子:
  1. public class TestApplication1 implements IApplication{  
  2.   
  3.         @Override  
  4.         public void init() {  
  5.               System. out.println("TestApplication1-->init" );  
  6.        }  
  7.   
  8.         @Override  
  9.         public void execute() {  
  10.               System. out.println("TestApplication1-->do something" );  
  11.        }  
  12.   
  13.         @Override  
  14.         public void destory() {  
  15.               System. out.println("TestApplication1-->destoryed" );  
  16.        }  
  17.   
  18. }  



是不是很簡單?對,就是這麼簡單。你可以照這個樣子,再寫一個獨立應用。接下來,你還需要在applications.xml中進行配置,很簡單,就是在apps標籤中增加如下程式碼:
  1. <app>  
  2.        <name> TestApplication1</name >  
  3.        <file> com.ijavaboy.app.TestApplication1</file >  
  4. </app>  


接下來,進入到本文的核心部分了,接下來我們的任務,就全部集中在熱部署上了,其實,也許現在你還覺得熱部署很神祕,但是,我相信一分鐘之後,你就不會這麼想了。要實現熱部署,我們之前說過,需要一個監聽器,來監聽釋出目錄applications,這樣當某個應用程式的jar檔案改變時,我們可以進行熱部署處理。其實,要實現目錄檔案改變的監聽,有很多種方法,這個例子中我使用的是apache的一個開源虛擬檔案系統——common-vfs。如果你對其感興趣,你可以訪問http://commons.apache.org/proper/commons-vfs/。這裡,我們繼承其FileListener介面,實現fileChanged 即可:

  1. public void fileChanged (FileChangeEvent event) throws Exception {  
  2.   
  3.       String ext = event.getFile().getName().getExtension();  
  4.        if(!"jar" .equalsIgnoreCase(ext)){  
  5.               return;  
  6.       }  
  7.         
  8.       String name = event.getFile().getName().getParent().getBaseName();  
  9.         
  10.       ApplicationManager. getInstance().reloadApplication(name);  
  11.         


當某個檔案改變的時候,該方法會被回撥。所以,我們在這個方法中呼叫了ApplicationManager的reloadApplication方法,重現載入該應用程式。

  1. public void reloadApplication (String name){  
  2.       IApplication oldApp = this.apps .remove(name);  
  3.         
  4.        if(oldApp == null){  
  5.               return;  
  6.       }  
  7.         
  8.       oldApp.destory();     //call the destroy method in the user's application  
  9.         
  10.       AppConfig config = this.configManager .getConfig(name);  
  11.        if(config == null){  
  12.               return;  
  13.       }  
  14.         
  15.       createApplication(getBasePath(), config);  


重現載入應用程式時,我們首先從記憶體中刪除該應用程式,然後呼叫原來應用程式的destory方法,最後按照配置重新建立該應用程式例項。
到這裡,你還覺得熱部署很玄妙很高深嗎?一切就是如此簡單。好了,言歸正傳,為了讓我們自定義的監聽介面可以有效工作起來,我們還需要指定它要監聽的目錄:

  1.  public void initMonitorForChange(String basePath){  
  2.         try {  
  3.                this.fileManager = VFS.getManager();  
  4.                 
  5.               File file = new File(basePath + GlobalSetting.JAR_FOLDER);  
  6.               FileObject monitoredDir = this.fileManager .resolveFile(file.getAbsolutePath());  
  7.               FileListener fileMonitorListener = new JarFileChangeListener();  
  8.                this.fileMonitor = new DefaultFileMonitor(fileMonitorListener);  
  9.                this.fileMonitor .setRecursive(true);  
  10.                this.fileMonitor .addFile(monitoredDir);  
  11.                this.fileMonitor .start();  
  12.               System. out.println("Now to listen " + monitoredDir.getName().getPath());  
  13.                 
  14.        } catch (FileSystemException e) {  
  15.               e.printStackTrace();  
  16.        }  
  17. }  


這裡,就是初始化監聽器的地方,我們使用VFS的DefaultFileMonitor完成監聽。而監聽的目錄,就是應用釋出目錄applications。接下來,為了讓整個應用程式可以持續的執行而不會結束,我們修改下啟動方法:
  1.  public static void main(String[] args){  
  2.          
  3.        Thread t = new Thread(new Runnable() {  
  4.                 
  5.                @Override  
  6.                public void run() {  
  7.                     ApplicationManager manager = ApplicationManager.getInstance();  
  8.                     manager.init();  
  9.               }  
  10.        });  
  11.          
  12.        t.start();  
  13.          
  14.         while(true ){  
  15.                try {  
  16.                     Thread. sleep(300);  
  17.               } catch (InterruptedException e) {  
  18.                     e.printStackTrace();  
  19.               }  
  20.        }  
  21. }  


好了,到這裡,一切都要結束了。現在,你已經很明白熱部署是怎麼一回事了,對嗎?不明白?OK,還有最後一招,去看看原始碼吧!

原始碼我已經放到了GitHub上面了,地址:https://github.com/chenjie19891104/ijavaboy/tree/master/AppLoader,歡迎下載使用,你擁有一切的權利對其進行修改。

最後,如果本文有什麼地方說的不準確,歡迎指正,謝謝!

相關文章