zoukankan      html  css  js  c++  java
  • 使用URLClassLoader动态加载jar

    背景介绍

    在某些项目中会使用插件化技术实现一些动态“插拔”或热更新的功能。一般的做法是,定义一个标准接口,然后将实现分离进行独立部署或更新。

    现在有个场景,系统希望引入一些特殊的业务“函数”,并支持热更新。来看看我们是怎么实现的。

    业务函数接口:IFunction.java

    /** 业务函数接口 **/
    public interface IFunction {
    
        /** 函数名称 **/
        public String getName();
    
        /** 函数描述 **/
        public String getDesc();
    
        /** 函数运行异常时返回默认值 **/
        public Object getDefVal();
    
        /** 调用函数 **/
        public Object process(Object... args) throws Exception;
    
        /** 检查入参是否为空 **/
        default boolean checkArgsIsEmpty(Object... args) {
            System.out.println(">> args=" + Arrays.toString(args));
            return args == null || args.length == 0;
        }
    }
    View Code

    函数调用工具类:FunctionUtil.java

    public class FunctionUtil {
        private static Map<String, IFunction> FUNCTIONS = null;
    
        protected FunctionUtil() {
        }
    
        private static Map<String, IFunction> getFunctions() {
            return FUNCTIONS;
        }
    
        /** call by CronJob.updateFunction() **/
        protected static synchronized void setFunctions(Map<String, IFunction> functions) {
            FUNCTIONS = functions;
        }
    
        /** load functions from jar file **/
        public static Map<String, IFunction> loadFunctions(URL jar) {
            Map<String, IFunction> functions = new ConcurrentHashMap<String, IFunction>();
            try {
                JarURLClassLoader classLoader = new JarURLClassLoader(jar);
                Set<Class> classes = classLoader.loadClass(IFunction.class, "com.example.function");
                if (classes != null && classes.size() > 0) {
                    for (Class clazz : classes) {
                        IFunction function = (IFunction) clazz.newInstance();
                        String name = function.getName();
                        functions.put(name, function);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return functions;
        }
    
        private static IFunction getFunction(String name) {
            Map<String, IFunction> functions = getFunctions();
            if (functions == null || functions.size() == 0) {
                return null;
            }
            return functions.get(name);
        }
    
        /** call the function **/
        @SuppressWarnings("unchecked")
        public static <T> T call(String name, Object... args) {
            IFunction function = getFunction(name);
            if (function == null) {
                System.err.println("function "" + name + "" not exist!");
                return null;
            }
            try {
                return (T) function.process(args);
            } catch (Exception e) {
                e.printStackTrace();
                return (T) function.getDefVal();
            }
        }
    
    }
    View Code

    支持从jar读取的类加载器:JarURLClassLoader.java

    public class JarURLClassLoader {
        private URL jar;
        private URLClassLoader classLoader;
    
        public JarURLClassLoader(URL jar) {
            this.jar = jar;
            classLoader = new URLClassLoader(new URL[] { jar });
        }
    
        /**
         * 在指定包路径下加载子类
         * 
         * @param superClass
         * @param pkgName
         * @return
         */
        public Set<Class> loadClass(Class<?> superClass, String basePackage) {
            JarFile jarFile;
            try {
                jarFile = ((JarURLConnection) jar.openConnection()).getJarFile();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
            return loadClassFromJar(superClass, basePackage, jarFile);
        }
    
        private Set<Class> loadClassFromJar(Class<?> superClass, String basePackage, JarFile jar) {
            Set<Class> classes = new HashSet<>();
            String pkgPath = basePackage.replace(".", "/");
            Enumeration<JarEntry> entries = jar.entries();
            Class<?> clazz;
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                String entryName = jarEntry.getName();
                if (entryName.charAt(0) == '/') {
                    entryName = entryName.substring(1);
                }
                if (jarEntry.isDirectory() || !entryName.startsWith(pkgPath) || !entryName.endsWith(".class")) {
                    continue;
                }
                String className = entryName.substring(0, entryName.length() - 6);
                clazz = loadClass(className.replace("/", "."));
                if (clazz != null && !clazz.isInterface() && superClass.isAssignableFrom(clazz)) {
                    classes.add(clazz);
                }
            }
            return classes;
        }
    
        private Class<?> loadClass(String name) {
            try {
                return classLoader.loadClass(name);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            return null;
        }
    
    }
    View Code

    将IFunction的实现分离,放在独立的工程内,如下图:

    Base64Encode.java

    public class Base64Encode implements IFunction {
    
        @Override
        public String getName() {
            return "base64Encode";
        }
    
        @Override
        public String getDesc() {
            return "Base64加密";
        }
        
        @Override
        public Object getDefVal() {
            return "";
        }
    
        @Override
        public Object process(Object... args) throws Exception {
            if (checkArgsIsEmpty(args)) {
                return "";
            }
            String s = (String) args[0];
            return Base64.getEncoder().encodeToString(s.getBytes());
        }
        
    }
    View Code

     将BizFunction打包成jar,部署在可供访问的服务器上,如:http://192.168.1.1:8000/biz-functions-v1.0.jar

    热更新的方式一般有2种:

    1.定时刷新,如发现jar文件发生变化则重新加载;

    2-动态触发,下发指定的更新动作进行重新加载;

    方式1的简单实现 :

    application.propertis

    # 网络加载
    function
    .jar.url=http://192.168.1.100:8080/plugins/biz-functions-v1.0.jar # 本地加载 function.jar.url=file:///usr/local/app/plugins/biz-functions-v1.0.jar

    CronJob.java

    @Configuration
    @EnableScheduling
    public class CronJob {
        @Value("${function.jar.url}")
        private String jarUrl;
    
        // 更新函数的定时任务
        @Scheduled(fixedDelay = 5000)
        public void updateFunction() {
            try {
                UpdateFunctionUtil.updateIfModified(jarUrl);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        // 更新函数的内部工具类
        private static class UpdateFunctionUtil extends FunctionUtil {
            private static long lastModified = 0L;
    
            private static synchronized void updateIfModified(String jarUrl) throws Exception {
                URL jar = new URL("jar:" + jarUrl + "!/");
                long modified = jar.openConnection().getLastModified();
                // 判断jar是否发生变化
                if (lastModified == modified) {
                    return;
                } else {
                    // 保存最新的修改时间
                    lastModified = modified;
                }
                Map<String, IFunction> functions = loadFunctions(jar);
                setFunctions(functions);
            }
        }
    }

    >> OK, THIS IS IT! 

  • 相关阅读:
    委托返回类型的协变性
    委托参数的逆变性
    单例采用双锁定技术
    Jupyter Notebook 工作空间 / 默认路径 的设置方式
    Runaway argument错误 [Overleaf: 在线Latex] [Type 3问题后续]
    [Android ADB] An auto-input method for Android and Windows
    [Latex] 所有字体embedded: Type3 PDF文档处理 / True Type转换为Type 1
    螺旋矩陣 非数组解法
    2014.9.11 Research Meeting Report
    2014.8.23 Research Meeting Report
  • 原文地址:https://www.cnblogs.com/lichmama/p/12858517.html
Copyright © 2011-2022 走看看