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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM