使用CustomClassLoader卸载Java类

问题描述 投票:0回答:1

我经历了很多帖子和问题,但没有一个人有一个绝对的Java程序来实现它。

要求:由于某些原因,我的应用程序加载了Common-codec 1.3.jar,

然后,在同一个jvm中,进程需要使用不同版本的Common-code 1.10.jar。

但是由于先前的类已加载并且它们具有相同的包名称,因此使用Java程序重新加载它们并不会替换现有的类。

这是我用来重新加载(替换)现有密钥的代码(示例),但没有找到预期的运气。请让我知道这可以通过Java示例来完成。

String pathToJar=/root/Desktop/commons-codec-1.10.jar";

            JarFile jarFile = null;
            try {
                jarFile = new JarFile(pathToJar);
            } catch (IOException e) {
                e.printStackTrace();
            }
            Enumeration<JarEntry> e = jarFile.entries();

            URL[] urls = new URL[0];
            try {
                urls = new URL[]{ new URL("jar:file:" + pathToJar+"!/") };
            } catch (MalformedURLException e1) {
                e1.printStackTrace();
            }
            URLClassLoader cl = URLClassLoader.newInstance(urls);

            while (e.hasMoreElements()) {
                JarEntry je = e.nextElement();
                if(je.isDirectory() || !je.getName().endsWith(".class")){
                    continue;
                }
                // -6 because of .class
                String className = je.getName().substring(0,je.getName().length()-6);
                className = className.replace(File.separatorChar, '.');
                String check="org.apache.commons.codec.binary.Base32";

                try {
                    Class c = cl.loadClass(className);   // Excepting it to replace old files,but thats not happening
                } catch (ClassNotFoundException e1) {
                    e1.printStackTrace();
                }

我被建议编写自定义类加载器并通过它可以卸载。有些人可能会展示一些相关的代码或过程。

classloader urlclassloader
1个回答
1
投票

只有当Class和定义它的ClassLoader都有资格进行垃圾收集时,才会发生“卸载”类。此外,任何给定的加载器只能定义一次特定名称的类。因此(除了仪表或JDK实现内部的异乎寻常的解决方法),没有“重装能力的类加载器”这样的东西 - 至少不是真正意义上的术语;为了获得类的不同“版本”,必须在n个不同的defineClass(C)对象上调用C,ClassLoader

让我们来看看以下简单的用例:我们有一个app.jar,(不出所料)包含一个声明main方法的类;和lib.jar的两个副本,每个都有一个类LibApiImpl,封装了一个只打印一个版本相关的字符串到stdout的方法。我们的目标是仅从main引用两个“库”类的副本,并看到输出两个不同的字符串。本文的其余部分仅展示了满足此要求的所有可能方法中的两种。

方法#1 - 手动实例化ClassLoaders

直接的解决方案是每次需要加载不同的URLClassLoader时简单地创建一个新的LibApiImpl。不需要自定义类加载器实现,默认的父级优先委托模型同时为应用程序和库提供服务。有几点需要注意:

  • 库JAR不能位于类路径上,以便默认的应用程序类加载器无法发现它们。否则,应用程序类加载器将“赞成”一个,而且,由于默认的委托模型,应用程序加载器的标准URLClassLoader子项将无法覆盖其父项对此问题的意见。
  • 要访问库类,必须使用3参数Class::forName,指定手动实例化的类加载器。单arg版本将委托给应用程序类加载器,它(根据前一点)当然不知道相应的.class文件的存在。
  • 如果将反射获得的类转换为接口,则如本文中的情况那样,与实现相反,接口必须驻留在类路径上。

Demo code

package com.example.app;

import java.net.URL;
import java.net.URLClassLoader;

import com.example.lib.api.LibApi;

public class App {

    public static void main(String... args) throws Exception {
        try (URLClassLoader loader = new URLClassLoader(new URL[] { new URL("file:/path/to/lib1.jar") })) {
            ((LibApi) Class.forName("com.example.lib.impl.LibApiImpl", true, loader).newInstance()).printVersionInfo();
        }
        try (URLClassLoader loader = new URLClassLoader(new URL[] { new URL("file:/path/to/lib2.jar") })) {
            ((LibApi) Class.forName("com.example.lib.impl.LibApiImpl", true, loader).newInstance()).printVersionInfo();
        }
    }

}

package com.example.lib.api;

public interface LibApi {

    void printVersionInfo();

}

package com.example.lib.impl;

import com.example.lib.api.LibApi;

public class LibApiImpl implements LibApi {

    @Override
    public void printVersionInfo() {
        System.out.println("\n** lib " + getClass() + " / loaded by " + getClass().getClassLoader() + " **\n");
    }

}

Packaging

纠正com.example.app.App的路径;然后产生以下4个JAR:

  • 包含app.jar包装的com.example.app
  • 包含lib-api.jar包装的com.example.lib.api
  • 两个“版本”(只出口两次),lib1.jarlib2.jar,每个包含com.example.lib.impl包。

Testing

运行如下:

java -cp '/path/to/app.jar:/path/to/lib-api.jar' com.example.app.App

样本输出:

** lib class com.example.lib.impl.LibApiImpl / loaded by java.net.URLClassLoader@55f96302 **


** lib class com.example.lib.impl.LibApiImpl / loaded by java.net.URLClassLoader@135fbaa4 **

方法#2 - 创建“主从”加载器

有时候,“父母优先”模式是不够的。作为一个人为的例子,假设我们想要获取LibApiImpl的“副本”,该副本最后由应用程序类加载器的某个子项加载,而不必关心实际定义该副本的子项。换句话说,我们希望调用Class.forName("com.example.lib.impl.LibApiImpl")返回“最新鲜”的LibApiImpl版本。但是,除非我们使用自定义实现覆盖应用程序类加载器,或者更常见的是App类的类加载器,否则该调用将始终失败,因为在默认委派模型下,委派从低级加载器单向流动他们的祖先,而不是他们的祖先。

下面给出的应用程序类加载器实现的行为如下(有关具体说明,请参阅Loaders类的Javadoc概述):有一个“主”加载器,作为应用程序类加载器,可能有一个子进程,被引用作为它的“奴隶”。 master负责从类路径加载不可重载的应用程序类(在本例中为app.jarlib-api.jar),而slave负责加载可重新加载的非类路径驻留(lib1.jarlib2.jar)。两者之间的通信是双向的,并且最终定义任何给定类“”固定“或”可重新加载“的加载器总是分别是主设备和从设备,而不管”启动“加载器,即,在其上的加载器。应用程序称为loadClass,或传递给Class::forName。当然,这只不过是一个玩具实现,旨在(希望)说明一个不同的授权方案可能会是什么样子,并且可能存在我尚未想象的方式的缺陷。例如,实际的实现必须提供适当的并发性;提供getResource等人的合规实施;解决代码可访问性,验证以及可能的代码库权限分配问题;并允许多个从属的可扩展性和配置,甚至可以将任意实现的其他子节点附加到主节点,同时保留明确定义的委托语义。当然,当有OSGi之类的东西(其中包括许多其他东西)时,编写正确的实现通常需要付出太多的努力。

Demo code

package com.example.app;

import java.net.URL;


import com.example.app.Loaders.MasterLoader;
import com.example.lib.api.LibApi;

public class App2 {

    public static void main(String... args) throws Exception {

        MasterLoader loader = (MasterLoader) ClassLoader.getSystemClassLoader();
        loader.setDebug(true);
        loader.refresh(new URL[] { new URL("file:/path/to/lib1.jar") });

        newLibApi().printVersionInfo();

        loader.refresh(new URL[] { new URL("file:/path/to/lib2.jar") });

        newLibApi().printVersionInfo();

    }

    static LibApi newLibApi() throws Exception {
        return (LibApi) Class.forName("com.example.lib.impl.LibApiImpl").newInstance();
    }

}

package com.example.app;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

/**
 * Contains a "Master (parent)/Slave (child)" <code>ClassLoader</code> "duo".<br/>
 * <br/>
 * The class loading "protocol" adhered by the respective {@code loadClass(String, boolean)}
 * implementations of the two is as follows:
 * <ol>
 * <li>If the name argument matches the name of the loader's class itself, return that class.
 * Otherwise proceed.</li>
 * <li>If a call to {@code findLoadedClass(name)} yields a non-null class, return that class.
 * Otherwise proceed.</li>
 * <li>Let <em>C</em> be either this loader's parent, if this is a "slave", or its child, if this is
 * a "master". If <em>C</em> is non-null, have it execute steps (1), (2) itself. If a class gets
 * produced, return that class. Otherwise (i.e., if there either is no result, or <em>C</em> is
 * null) proceed.</li>
 * <li>If the name argument refers to a potential bootstrap classpath class name, call
 * {@code loadClass(name)} on the default system classloader (the "master's" parent). If the call
 * succeeds, return that class. Otherwise proceed.</li>
 * <li>If the name argument refers to a .class file under this loader's search path, read it, define
 * and link a new class off of its contents, and return that "freshly-fefined" class. Otherwise
 * proceed.</li>
 * <li>Once again, let <em>C</em> be the loader specified in step (3). If non-null, have it execute
 * step (5) on itself. If a class gets produced, return that class. Otherwise fail.</li>
 * </ol>
 */
public class Loaders {

    private static class SlaveLoader extends URLClassLoader {

        static final Pattern BOOT_CLASS_PATH_RES_NAMES = Pattern.compile("((com\\.)?sun|java(x)?)\\..*");
        static final URL[] EMPTY_SEARCH_PATH = new URL[0];

        static final URL[] createSearchPath(String pathNames) {
            if (pathNames != null) {
                List<URL> searchPath = new ArrayList<>();
                for (String pathName : pathNames.split(File.pathSeparator)) {
                    try {
                        searchPath.add(Paths.get(pathName).toUri().toURL());
                    }
                    catch (MalformedURLException e) {
                        e.printStackTrace();
                    }
                }
                return searchPath.toArray(new URL[0]);
            }
            return EMPTY_SEARCH_PATH;
        }

        static final byte[] readClassData(URL classResource) throws IOException {
            try (InputStream in = classResource.openStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                while (in.available() > 0) {
                    out.write(in.read());
                }
                return out.toByteArray();
            }
        }

        final String loadClassOutcomeMsgFmt = "loadClass return '{'\n\tloader = {0}\n\ttarget = {1}\n\tresult : {2}\n'}'";
        volatile boolean debug;

        SlaveLoader(URL[] searchPath, ClassLoader parent) {
            super(searchPath, parent);
        }

        @Override
        protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            validateName(name);
            Class<?> ret = loadFromCache(name);
            if (ret != null) {
                return ret;
            }
            MasterLoader parent = (MasterLoader) getParent();
            if ((ret = parent.loadFromCache(name)) != null) {
                log(loadClassOutcomeMsgFmt, this, name, "3 - early return - pre-loaded/cached - via " + parent);
                return ret;
            }
            if ((ret = loadFromBootClasspath(name)) != null) {
                return ret;
            }
            if ((ret = loadFromSearchPath(name, resolve)) != null) {
                return ret;
            }
            if ((ret = parent.loadFromSearchPath(name, resolve)) != null) {
                log(loadClassOutcomeMsgFmt, this, name,
                        "6 - common/non-reloadable classpath delegation - via " + parent);
                return ret;
            }
            if ((ret = parent.loadFromSearchPath(name, resolve)) != null) {
                return ret;
            }
            throw createCnfe(name, null);
        }

        void validateName(String name) throws ClassNotFoundException {
            log("loadClass entry '{'\n\tloader = {0}\n\ttarget = {1}\n'}'", this, name);
            if ((name == null) || name.trim().isEmpty()) {
                throw createCnfe(name, null);
            }
        }

        Class<?> loadFromCache(String name) {
            Class<?> ret = getClass();
            if (ret.getName().equals(name)) {
                log(loadClassOutcomeMsgFmt, this, name, "1 - early return - own class");
                return ret;
            }
            if ((ret = findLoadedClass(name)) != null) {
                log(loadClassOutcomeMsgFmt, this, name, "2 - early return - pre-loaded/cached");
                return ret;
            }
            return null;
        }

        Class<?> loadFromBootClasspath(String name) {
            if (BOOT_CLASS_PATH_RES_NAMES.matcher(name).matches()) {
                ClassLoader defSysCl = ClassLoader.getSystemClassLoader().getParent();
                try {
                    Class<?> ret = ClassLoader.getSystemClassLoader().getParent().loadClass(name);
                    log(loadClassOutcomeMsgFmt, this, name, "4 - bootstrap classpath delegation - via " + defSysCl);
                    return ret;
                }
                catch (ClassNotFoundException cnfe) {
                }
            }
            return null;
        }

        Class<?> loadFromSearchPath(String name, boolean resolve) throws ClassNotFoundException {
            Class<?> ret = null;
            URL res = findResource(name.replace(".", "/") + ".class");
            if (res != null) {
                byte[] b;
                try {
                    b = readClassData(res);
                }
                catch (IOException ioe) {
                    throw createCnfe(name, ioe);
                }
                ret = defineClass(name, b, 0, b.length);
                if (resolve) {
                    resolveClass(ret);
                }
                log(loadClassOutcomeMsgFmt, this, name, "5 - freshly-defined from local search path");
                return ret;
            }
            return null;
        }

        ClassNotFoundException createCnfe(String name, Throwable cause) throws ClassNotFoundException {
            return new ClassNotFoundException(MessageFormat.format("Class loading : {0} : {1} : FAILED", this,
                    (name == null) ? "null" : name, cause));
        }

        void log(String msg, Object... args) {
            if (debug) {
                System.out.println(MessageFormat.format("\n" + msg + "\n", args));
            }
        }

        public void setDebug(boolean debug) {
            this.debug = debug;
        }

        @Override
        protected void finalize() throws Throwable {
            try {
                close();
            }
            finally {
                super.finalize();
            }
            log("ClassLoader finalization : {0}", this);
        }

    }

    public static class MasterLoader extends SlaveLoader {

        static final URL[] DEFAULT_CLASS_PATH = createSearchPath(System.getProperty("java.class.path"));

        private URL[] reloadableSearchPath = EMPTY_SEARCH_PATH;
        private volatile SlaveLoader slave;

        public MasterLoader(ClassLoader parent) {
            super(DEFAULT_CLASS_PATH, parent);
        }

        public synchronized void refresh(URL[] reloadableSearchPath) {
            int len;
            if ((reloadableSearchPath != null) && ((len = reloadableSearchPath.length) > 0)) {
                List<URL> path = new ArrayList<>(len + 1);
                for (int i = 0; i < len; i++) {
                    URL entry = reloadableSearchPath[i];
                    if (entry != null) {
                        path.add(entry);
                    }
                }
                this.reloadableSearchPath = (!path.isEmpty()) ? path.toArray(EMPTY_SEARCH_PATH) : EMPTY_SEARCH_PATH;
            }
            else {
                this.reloadableSearchPath = EMPTY_SEARCH_PATH;
            }
            if (slave != null) {
                try {
                    slave.close();
                }
                catch (IOException ioe) {
                }
                slave = null;
                /*
                 * At least two calls to System::gc appear to be required in order for Class::forName to cease
                 * returning cached classes previously defined by slave and for which the master served as an
                 * intermediary, i.e., an "initiating loader".
                 * 
                 * See also http://blog.hargrave.io/2007/09/classforname-caches-defined-class-in.html
                 */
                for (int i = 0; i < 2; i++) {
                    System.gc();
                    try {
                        Thread.sleep(100);
                    }
                    catch (InterruptedException ie) {
                    }
                }
            }
            if (this.reloadableSearchPath != EMPTY_SEARCH_PATH) {
                log("Class loader search path refresh : {0}\n\tSearch path = {1}", this,
                        Arrays.toString(this.reloadableSearchPath));
                slave = new SlaveLoader(this.reloadableSearchPath, this);
                slave.setDebug(debug);
            }
        }

        @Override
        protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            validateName(name);
            Class<?> ret = loadFromCache(name);
            if (ret != null) {
                return ret;
            }
            if ((slave != null) && ((ret = slave.loadFromCache(name)) != null)) {
                log(loadClassOutcomeMsgFmt, this, name, "3 - early return - pre-loaded/cached - via " + slave);
                return ret;
            }
            if ((ret = loadFromBootClasspath(name)) != null) {
                return ret;
            }
            if ((ret = loadFromSearchPath(name, resolve)) != null) {
                return ret;
            }
            if ((slave != null) && ((ret = slave.loadFromSearchPath(name, resolve)) != null)) {
                log(loadClassOutcomeMsgFmt, this, name,
                        "6 - reloadable classpath delegation - via " + ret.getClassLoader());
                return ret;
            }
            throw createCnfe(name, null);
        }

    }

}

Packaging

再次纠正com.example.app.App2中的路径;将App2com.example.app.Loaders添加到app.jar;和再出口。其他JAR应保持与前一个示例相同。

Testing

运行如下:

java -cp '/path/to/app.jar:/path/to/lib-api.jar' \
'-Djava.system.class.loader=com.example.app.Loaders$MasterLoader' \
com.example.app.App2

示例输出(省略loadClass调试):

** lib class com.example.lib.impl.LibApiImpl / loaded by com.example.app.Loaders$SlaveLoader@7f31245a **

...

ClassLoader finalization : com.example.app.Loaders$SlaveLoader@7f31245a

...

** lib class com.example.lib.impl.LibApiImpl / loaded by com.example.app.Loaders$SlaveLoader@12a3a380 **
© www.soinside.com 2019 - 2024. All rights reserved.