这两天做的项目中按照客户要求需要将插件模式应用到本项目中,以达到客户可以自己动态增加相关功能的目的,然后我们就根据需求制定出接口,再由客户自己实现接口,通过项目提供的相应界面将实现的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:本人不善言辞写的不好观者多多包涵!哈哈...
,