jar包 热加载/卸载 的初步实现

来源:互联网 发布:js radiobutton 编辑:程序博客网 时间:2024/06/05 07:37

  这两天做的项目中按照客户要求需要将插件模式应用到本项目中,以达到客户可以自己动态增加相关功能的目的,然后我们就根据需求制定出接口,再由客户自己实现接口,通过项目提供的相应界面将实现的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 strawxdl14  */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,方便以后卸载jar24      * @param 一个可想类加载器的classpath中添加的文件url25      */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 block43             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.PluginManagerload pluginload plugin  successdo plugincom.tds.test.classloader.Plugin1Plugin1 doSome ... 我可以?unload pluginUnloading plugin JAR file D:\testclassloader\plugin.jarload pluginload plugin  successdo plugincom.tds.test.classloader.Plugin1Plugin1 doSome ... 我不可以?
复制代码
原创粉丝点击