最近在开发一个应用的过程中,需要支持加载外部的jar包,最初的想法是自定义一个ClassLoader,加载外部jar包,这样就能搞定了,于是google了一下自定义ClassLoader,参考着写了一个loader,代码如下:

public final class DynamicExtensionLoader extends URLClassLoader {

    private static final Logger log = LoggerFactory.getLogger(DynamicExtensionLoader.class);
    //保存本类加载器加载的class
    private static final Map<String, byte[]> BYTE_CODE_MAP = new HashMap<>();

    public DynamicExtensionLoader(URL[] urls) {
        super(urls);
        this.init(urls);
    }

    private void init(URL[] urls){
        try{
            for(URL url : urls) {
                this.loadClasses(new JarFile(url.getPath()));
            }
        }catch(Exception e){
            log.error("init", e);
        }
    }

    @Override
    public Class<?> findClass(final String name) throws ClassNotFoundException{
        if(BYTE_CODE_MAP.containsKey(name)){
            byte[] byteCode = BYTE_CODE_MAP.get(name);
            return this.defineClass(name, byteCode, 0, byteCode.length);
        }else{
            return super.findClass(name);
        }
    }

    private void loadClasses(JarFile jarFile){
        Enumeration<JarEntry> en = jarFile.entries();
        InputStream input = null;
        try{
            while (en.hasMoreElements()) {
                JarEntry je = en.nextElement();
                String name = je.getName();
                //这里添加了路径扫描限制
                if (name.endsWith(".class")) {
                    String className = name.replace(".class", "").replaceAll("/", ".");
                    input = jarFile.getInputStream(je);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int bufferSize = 4096;
                    byte[] buffer = new byte[bufferSize];
                    int bytesNumRead = 0;
                    while ((bytesNumRead = input.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesNumRead);
                    }
                    byte[] classBytes = baos.toByteArray();
                    BYTE_CODE_MAP.put(className, classBytes);
                }
            }
        } catch (IOException e) {
            log.error("loadClasses", e);
        } finally {
            if(input!=null){
                try {
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        for (Map.Entry<String, byte[]> entry : BYTE_CODE_MAP.entrySet()) {
            String key = entry.getKey();
            try {
                //载入所有的class
                this.loadClass(key);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoClassDefFoundError e){
                //忽略jar中的三方依赖缺少的异常
                System.out.println("NoClassDefFoundError =>" + key);
            }
        }
    }
}

通过测试,确实可以加载外部的jar,但马上发现了一个问题,使用自定义ClassLoader加载的类,在使用的时候,也需要用自定义的loader来加载,非常的不方便。如是又想到将外部的jar加载到系统的中loader中,于是就想直接获取系统的ClassLoader,将jar加载进去不就可以了么。

说干就干,通过ClassLoader.getSystemClassLoader()能获取到AppClassLoader,而AppClassLoader又继承自URLClassLoader,刚好URLClassLoader有个addURL的方法可以将外部jar包加入应用内的ClassLoader,但是addURL是个受保护的方法,因此需要使用反射来调用这个方法,完整代码如下:

public class LoadExtLibsUtil {

    private static final Logger logger = LoggerFactory.getLogger(LoadExtLibsUtil.class);

    /**
     * 加载目录下的所有jar文件
     * @param jarPath
     */
    public static void loadJars(String jarPath){
        File jarDir = new File(jarPath);
        if(jarDir.isDirectory() && jarDir.canRead()){
            File[] jars = jarDir.listFiles();
            try{
                for(File jar : jars){
                    loadURL(jar.toURI().toURL());
                }
            }catch (Exception e){
                logger.error("loadJars", e);
            }
        }
    }

    /**
     * 通过反映将jar包加入系统ClassLoader
     * @param url
     */
    private static void loadURL(URL url){
        if(url!=null){
            Method addUrl = null;
            boolean isAccessible = false;
            try{
                addUrl = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
                isAccessible = addUrl.isAccessible();
                addUrl.setAccessible(true);
                URLClassLoader loader = (URLClassLoader) ClassLoader.getSystemClassLoader();
                addUrl.invoke(loader, url);
            }catch (Exception e){
                logger.error("loadURL", e);
            }finally {
                if(addUrl!=null){
                    addUrl.setAccessible(isAccessible);
                }
            }
        }
    }
}

发表评论