使用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! 

posted @ 2020-05-09 16:28  lichmama  阅读(8087)  评论(3编辑  收藏  举报