背景介绍
在某些项目中会使用插件化技术实现一些动态“插拔”或热更新的功能。一般的做法是,定义一个标准接口,然后将实现分离进行独立部署或更新。
现在有个场景,系统希望引入一些特殊的业务“函数”,并支持热更新。来看看我们是怎么实现的。
业务函数接口: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; } }
函数调用工具类: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(); } } }
支持从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; } }
将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()); } }
将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!