這兩天做的項目中按照客戶要求需要將插件模式應用到本項目中,以達到客戶可以自己動態增加相關功能的目的,然后我們就根據需求制定出接口,再由客戶自己實現接口,通過項目提供的相應界面將實現的jar包上傳,由服務器應用對jar包進行熱加載/卸載,jar包的熱加載用java原生的一些api即可實現,但問題是,使用原生的api的話,是無法實現卸載jar包的功能的,除非重啟應用,但又因為插件的基本特征就是熱加載,熱卸載,熱啟動等等熱的問題(總之就是熱,呵呵),這樣的話,重啟應用就顯得有那么點不專業了。所以還是實現一個熱加載/卸載的功能好點,一開始不知道怎么下手,后來在研究openfire時發現其插件jar包可以熱加載/卸載,於是乎研究其插件加載方式。經過一天的代碼調試,發現openfire是通過繼承URLClassLoader實現了一個自己的PluginClassLoader(想看源碼的自己去openfire官網下吧,這里就不提供了),於是我對PluginClassLoader進行了一下改造,去掉了一些沒用的方法與代碼,留下關鍵部分(這些部分仍然是openfire原生的東西).改造以后的PluginCLassLoader代碼如下:
1 package com.tds.test.classloader; 2 3 import java.net.JarURLConnection; 4 import java.net.URL; 5 import java.net.URLClassLoader; 6 import java.net.URLConnection; 7 import java.util.ArrayList; 8 import java.util.List; 9 10 11 /** 12 * 插件類加載器,在插件目錄中搜索jar包,並為發現的資源(jar)構造一個類加載器,將對應的jar添加到classpath中 13 * @author strawxdl 14 */ 15 public class PluginClassLoader extends URLClassLoader { 16 17 private List<JarURLConnection> cachedJarFiles = new ArrayList<JarURLConnection>(); 18 public PluginClassLoader() { 19 super(new URL[] {}, findParentClassLoader()); 20 } 21 22 /** 23 * 將指定的文件url添加到類加載器的classpath中去,並緩存jar connection,方便以后卸載jar 24 * @param 一個可想類加載器的classpath中添加的文件url 25 */ 26 public void addURLFile(URL file) { 27 try { 28 // 打開並緩存文件url連接 29 30 URLConnection uc = file.openConnection(); 31 if (uc instanceof JarURLConnection) { 32 uc.setUseCaches(true); 33 ((JarURLConnection) uc).getManifest(); 34 cachedJarFiles.add((JarURLConnection)uc); 35 } 36 } catch (Exception e) { 37 System.err.println("Failed to cache plugin JAR file: " + file.toExternalForm()); 38 } 39 addURL(file); 40 } 41 42 /** 43 * 卸載jar包 44 */ 45 public void unloadJarFiles() { 46 for (JarURLConnection url : cachedJarFiles) { 47 try { 48 System.err.println("Unloading plugin JAR file " + url.getJarFile().getName()); 49 url.getJarFile().close(); 50 url=null; 51 } catch (Exception e) { 52 System.err.println("Failed to unload JAR file\n"+e); 53 } 54 } 55 } 56 57 /** 58 * 定位基於當前上下文的父類加載器 59 * @return 返回可用的父類加載器. 60 */ 61 private static ClassLoader findParentClassLoader() { 62 ClassLoader parent = PluginManager.class.getClassLoader(); 63 if (parent == null) { 64 parent = PluginClassLoader.class.getClassLoader(); 65 } 66 if (parent == null) { 67 parent = ClassLoader.getSystemClassLoader(); 68 } 69 return parent; 70 } 71 }
然后通過PluginManager.java對每個插件jar包的PluginClassLoader進行管理,PluginManager.java實現了jar包的加載(loadPlugin方法)與卸載(unloadPlugin方法),這里為了測試假設每一個插件jar包中實現了插件接口的類的package名均為com.tds.test.classloader.Plugin1,其中Plugin1即為實現了Plugin接口的類名(這兩個類稍后提供源碼),這里將其寫死在PluginManager中,在實際項目中當然每個插件的實現的package都會是不一樣的現在不深究這個問題。下邊上PluginManager.java代碼:
1 package com.tds.test.classloader; 2 3 import java.net.MalformedURLException; 4 import java.net.URL; 5 import java.util.HashMap; 6 import java.util.Map; 7 8 public class PluginManager { 9 static{ 10 System.out.println(PluginManager.class.getName()); 11 } 12 private Map<String ,PluginClassLoader> pluginMap = new HashMap<String,PluginClassLoader>(); 13 private static String packagename = "com.tds.test.classloader.Plugin1"; 14 public PluginManager(){ 15 16 } 17 18 public void doSome(String pluginName){ 19 20 try{ 21 Class<?> forName = Class.forName(packagename, true, getLoader(pluginName));//this.pluginMap.get(pluginName).loadClass(packagename); 22 Plugin ins = (Plugin)forName.newInstance(); 23 ins.doSome(); 24 }catch(Exception e){ 25 e.printStackTrace(); 26 } 27 } 28 private void addLoader(String pluginName,PluginClassLoader loader){ 29 this.pluginMap.put(pluginName, loader); 30 } 31 private PluginClassLoader getLoader(String pluginName){ 32 return this.pluginMap.get(pluginName); 33 } 34 public void loadPlugin(String pluginName){ 35 this.pluginMap.remove(pluginName); 36 PluginClassLoader loader = new PluginClassLoader(); 37 String pluginurl = "jar:file:/D:/testclassloader/"+pluginName+".jar!/"; 38 URL url = null; 39 try { 40 url = new URL(pluginurl); 41 } catch (MalformedURLException e) { 42 // TODO Auto-generated catch block 43 e.printStackTrace(); 44 } 45 loader.addURLFile(url); 46 addLoader(pluginName, loader); 47 System.out.println("load " + pluginName + " success"); 48 } 49 public void unloadPlugin(String pluginName){ 50 this.pluginMap.get(pluginName).unloadJarFiles(); 51 this.pluginMap.remove(pluginName); 52 } 53 }
下邊是接口類Plugin.java代碼:
1 package com.tds.test.classloader; 2 3 public interface Plugin { 4 5 public void doSome(); 6 }
下邊是接口Plugin.java實現類:Plugin1.java
1 package com.tds.test.classloader; 2 3 public class Plugin1 implements Plugin{ 10 public void doSome(){ 11 System.out.println("Plugin1 doSome ... 我不可以?"); 12 } 13 }
將上述Plugin1.java單獨導出為jar包,命名為plugin1.jar
修改Plugin1.java代碼,如下所示:
package com.tds.test.classloader; public class Plugin1 implements Plugin{ public void doSome(){ System.out.println("Plugin1 doSome ... 我可以?"); } }
將修改以后的Plugin1.java單獨導出為Plugin2.jar
這時Plugin1.jar和Plugin2.jar對Plugin接口的實現都是不同的了,測試主類如下:
1 package com.tds.test.classloader; 2 3 import java.io.BufferedReader; 4 import java.io.InputStreamReader; 5 6 public class TestMain { 7 8 public static void main(String[] args) throws Exception { 9 10 11 PluginManager manager = new PluginManager();; 12 13 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 14 String cmd = br.readLine(); 15 16 while(!cmd.equals("bye")){ 17 if(cmd.startsWith("do")){ 18 String pluginName = cmd.split(" ")[1]; 19 manager.doSome(pluginName); 20 } 21 if(cmd.startsWith("load")){ 22 String pluginName = cmd.split(" ")[1]; 23 manager.loadPlugin(pluginName); 24 } 25 if(cmd.startsWith("unload")){ 26 String pluginName = cmd.split(" ")[1]; 27 manager.unloadPlugin(pluginName); 28 } 29 cmd = br.readLine(); 30 } 31 } 32 }
測試方法為:在TestMain.java中run as JavaApplication,然后plugin1.jar放到D:/testclassloader/目錄下並改名為plugin.jar,最后在控制台輸入load plugin即可將plugin.jar使用自定義的PluginClassLoader進行裝載,這時再使用do plugin命令即可調用該插件的實現,現在卸載plugin.jar,輸入unload plugin命令即可卸載掉plugin.jar,接下來關鍵時刻到了刪除D:/testclassloader/目錄的plugin.jar,將plugin2.jar放入該目錄並更名為plugin.jar,重復之前的jar包加載命令並運行之,你會發現兩次運行的結果不一樣了下邊是我運行的示例:
com.tds.test.classloader.PluginManager load plugin load plugin success do plugin com.tds.test.classloader.Plugin1 Plugin1 doSome ... 我可以? unload plugin Unloading plugin JAR file D:\testclassloader\plugin.jar load plugin load plugin success do plugin com.tds.test.classloader.Plugin1 Plugin1 doSome ... 我不可以?
可能有的人看了以后不明白我做了這么多是為什么,這些人你們可以試一下用java原生的classloader進行jar包的加載,看看有沒有辦法卸載掉已經加載的jar包,最明顯的一個現象就是你如果不卸載掉該jar包的話,我上邊要求的刪除plugin.jar就無法刪除了,除非你把應用停掉,才可以刪除該jar包,我現在所做的這一切也都只是為了不需要你重啟或者停止服務器應用就可以直接更換相應的jar包,這個是插件模式必須的要求了。
最后關於PluginClassLoader類中addURLFile和unloadJarFiles兩個方法,我有點疑惑,這個類加載器也只是在addURLFile的時候將對應jar文件進行了URLConnection uc = file.openConnection();然后緩存了這個連接,在卸載jar時關閉了這個連接,這兩處的打開和關閉操作為什么會對jvm對相應jar包的加載產生影響?希望有明白的人看到本文后指點一二,不甚感激!!
PS:本人不善言辭寫的不好觀者多多包涵!哈哈...
,