背景介紹
在某些項目中會使用插件化技術實現一些動態“插拔”或熱更新的功能。一般的做法是,定義一個標准接口,然后將實現分離進行獨立部署或更新。
現在有個場景,系統希望引入一些特殊的業務“函數”,並支持熱更新。來看看我們是怎么實現的。
業務函數接口: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!