是否可以为Java创建自定义类加载器以从JAR或CLASS_PATH有条件地加载类?

时间:2017-07-25 21:25:38

标签: java classloader dynamic-class-loaders

我们在工作中讨论了静态(来自构建的JAR)与动态(来自CLASS_PATH中的单独位置)加载Java库的优缺点。

在讨论的中间,我想到了:不管哪一方是对的,也许还有办法让你的蛋糕吃掉它:

  • 为您的组织设置默认的自定义类加载器
  • 类加载器 - 加载特定库时 - 检查一个配置,对于每个库(或更多粒度的app +库组合),它包含一个标志,用于确定是从JAR静态加载库还是动态地从CLASS_PATH
  • 应用程序都是使用JAR中的库类构建的(如果您不想动态加载新库版本,则使用库的默认“备份”版本)
  • 如果您希望静态加载库(例如,因为新版本与旧版本不兼容,或者只是为了消除更改风险),请将该库的配置标志设置为true。
  • 如果您的更改风险为零/低,则将标志设置为false;并允许动态加载库,从而允许您发布所有应用程序获取的库的​​新版本,而无需重新编译和重新发布(显然,应用程序需要使用新库进行测试)除了一个臭虫的雷区之外什么都没有。)

我的想法是好还是坏,我想知道的是,我的第二个要点是否在技术上是可行的Java(比如1.8+),如果是,那么实施它会涉及什么吗

1 个答案:

答案 0 :(得分:2)

是的,绝对的。我将假设Java 8得到这个答案,因为我还没有熟悉Java 9的Module System的功能。

主要想法很简单 - ClassLoader loadClass(String, boolean)实施符合以下"协议":

  1. 如果类名参数引用系统类,则委托加载到父加载器并返回。否则继续。
  2. 根据您的配置,名称是否指的是应用程序类?如果是,则继续执行步骤2.1,否则执行步骤3。
    1. 获取应用程序"搜索路径" (根据问题中的要求,JAR文件本身的文件系统路径)。
    2. 尝试以特定于实现的方式(通过例如针对搜索路径的条目的路径解析)找到搜索路径下与name参数匹配的类资源。如果存在此类资源,请继续执行步骤5,否则执行3.
  3. 对于尚未检查的每个已知库,按降序排序:
    1. 名称是否指的是该库的类?如果是这样继续;否则返回3.
    2. 获取特定库的搜索路径。
    3. 尝试在搜索路径下找到与name参数匹配的类资源。如果成功,请继续执行步骤5,否则返回步骤3.
  4. 如果在步骤2或3中未建立匹配的类资源,则引发异常。否则继续。
  5. 检索资源的内容,根据需要委托给defineClassresolveClass,然后返回新课程。
  6. <小时/> 下面是这种ClassLoader的一个(非常难看的)示例实现。

    假设:

    • 模块应用程序
    • 模块包括一组已知的类;换句话说,模块&#34;知道其内容&#34;。
    • 应用程序使用零个或多个库。
    • 多个逻辑应用程序可能在同一个JVM上运行。
    • ConfigurationClassLoader进行通信:
      • &#34;当前&#34;应用程序请求类加载。
      • 应用程序和搜索路径之间的映射。
      • 库应用程序对和搜索路径之间的映射。
    • 多个加载器可以使用(持久表示)相同的配置。
    package com.example.q45313762;
    
    import java.io.BufferedInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.URISyntaxException;
    import java.net.URL;
    import java.net.URLClassLoader;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.security.CodeSource;
    import java.security.ProtectionDomain;
    import java.security.cert.Certificate;
    import java.util.Arrays;
    import java.util.Collections;
    import java.util.Enumeration;
    import java.util.Iterator;
    import java.util.LinkedHashMap;
    import java.util.LinkedHashSet;
    import java.util.Map;
    import java.util.Objects;
    import java.util.Set;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    import java.util.function.BiFunction;
    import java.util.function.Predicate;
    import java.util.function.Supplier;
    
    public final class ConfigurableClasspathClassLoader extends URLClassLoader {
    
        public interface Configuration {
    
            interface Module {
    
                String getName();
    
                String getVersion();
    
                boolean includes(String resourceName);
    
            }
    
            interface Library extends Module {}
    
            interface Application extends Module {}
    
            enum LoadingMode {
                STATIC, DYNAMIC;
            }
    
            Application getCurrentApplication();
    
            Iterable<URL> getLibrarySearchPath(Library lib, LoadingMode mode, Application app);
    
            Iterable<URL> getApplicationSearchPath(Application app);
    
            Iterable<Library> getApplicationLibraries(Application app);
    
        }
    
        public static final class SimpleStaticConfiguration implements Configuration {
    
            private static abstract class SimpleModule implements Module {
    
                private final String name, version;
                private final Predicate<String> resourceNameMatcher;
    
                private SimpleModule(String name, String version, Predicate<String> resourceNameMatcher) {
                    requireNoneNull(name, version, resourceNameMatcher);
                    name = name.trim();
                    version = version.trim();
                    if (name.isEmpty() || version.isEmpty()) {
                        throw new IllegalArgumentException("arguments must not be empty.");
                    }
                    this.name = name;
                    this.version = version;
                    this.resourceNameMatcher = resourceNameMatcher;
                }
    
                @Override
                public String getName() {
                    return name;
                }
    
                @Override
                public String getVersion() {
                    return version;
                }
    
                @Override
                public boolean includes(String resourceName) {
                    if (resourceName == null) {
                        return false;
                    }
                    return resourceNameMatcher.test(resourceName);
                }
    
                @Override
                public final int hashCode() {
                    final int prime = 31;
                    int result = 1;
                    result = prime * result + ((name == null) ? 0 : name.hashCode());
                    result = prime * result + ((resourceNameMatcher == null) ? 0 : resourceNameMatcher.hashCode());
                    result = prime * result + ((version == null) ? 0 : version.hashCode());
                    return result;
                }
    
                @Override
                public final boolean equals(Object obj) {
                    if (this == obj) {
                        return true;
                    }
                    if (obj == null) {
                        return false;
                    }
                    if (!(obj instanceof SimpleModule)) {
                        return false;
                    }
                    SimpleModule other = (SimpleModule) obj;
                    if (name == null) {
                        if (other.name != null) {
                            return false;
                        }
                    }
                    else if (!name.equals(other.name)) {
                        return false;
                    }
                    if (resourceNameMatcher == null) {
                        if (other.resourceNameMatcher != null) {
                            return false;
                        }
                    }
                    else if (!resourceNameMatcher.equals(other.resourceNameMatcher)) {
                        return false;
                    }
                    if (version == null) {
                        if (other.version != null) {
                            return false;
                        }
                    }
                    else if (!version.equals(other.version)) {
                        return false;
                    }
                    return true;
                }
    
            }
    
            public static final class SimpleLibrary extends SimpleModule implements Library {
    
                public SimpleLibrary(String name, String version, Predicate<String> resourceNameMatcher) {
                    super(name, version, resourceNameMatcher);
                }
    
            }
    
            public static final class SimpleApplication extends SimpleModule implements Application {
    
                public SimpleApplication(String name, String version, Predicate<String> resourceNameMatcher) {
                    super(name, version, resourceNameMatcher);
                }
    
            }
    
            private static final class ModuleRegistry {
    
                private static abstract class Key {
    
                    private final Module module;
    
                    private Key(Module module) {
                        requireNoneNull(module);
                        requireNoneNull(module.getName(), module.getVersion());
                        this.module = module;
                    }
    
                    private Module getModule() {
                        return module;
                    }
    
                }
    
                private static final class LibraryKey extends Key {
    
                    private final LoadingMode mode;
                    private final Application app;
    
                    private LibraryKey(Library lib, LoadingMode mode, Application app) {
                        super(lib);
                        requireNoneNull(mode);
                        requireNoneNull(app);
                        this.mode = mode;
                        this.app = app;
                    }
    
                    private Library getLibrary() {
                        return (Library) super.getModule();
                    }
    
                    private LoadingMode getLoadingMode() {
                        return mode;
                    }
    
                    private Application getApplication() {
                        return app;
                    }
    
                    @Override
                    public int hashCode() {
                        final int prime = 31;
                        int result = 1;
                        Library lib = getLibrary();
                        result = prime * result + ((lib == null) ? 0 : lib.hashCode());
                        result = prime * result + ((mode == null) ? 0 : mode.hashCode());
                        result = prime * result + ((app == null) ? 0 : app.hashCode());
                        return result;
                    }
    
                    @Override
                    public boolean equals(Object obj) {
                        if (this == obj) {
                            return true;
                        }
                        if (obj == null) {
                            return false;
                        }
                        if (!(obj instanceof LibraryKey)) {
                            return false;
                        }
                        LibraryKey other = (LibraryKey) obj;
                        Library thisLib = getLibrary(), othersLib = other.getLibrary();
                        if (thisLib == null) {
                            if (othersLib != null) {
                                return false;
                            }
                        }
                        else if (!thisLib.equals(othersLib)) {
                            return false;
                        }
                        if (mode != other.mode) {
                            return false;
                        }
                        if (app == null) {
                            if (other.app != null) {
                                return false;
                            }
                        }
                        else if (!app.equals(other.app)) {
                            return false;
                        }
                        return true;
                    }
    
                }
    
                private static final class ApplicationKey extends Key {
    
                    private ApplicationKey(Application app) {
                        super(app);
                    }
    
                    private Application getApplication() {
                        return (Application) super.getModule();
                    }
    
                    @Override
                    public int hashCode() {
                        final int prime = 31;
                        int result = 1;
                        Application app = getApplication();
                        result = prime * result + ((app == null) ? 0 : app.hashCode());
                        return result;
                    }
    
                    @Override
                    public boolean equals(Object obj) {
                        if (this == obj) {
                            return true;
                        }
                        if (obj == null) {
                            return false;
                        }
                        if (!(obj instanceof ApplicationKey)) {
                            return false;
                        }
                        ApplicationKey other = (ApplicationKey) obj;
                        Application thisApp = getApplication(), othersApp = other.getApplication();
                        if (thisApp == null) {
                            if (othersApp != null) {
                                return false;
                            }
                        }
                        else if (!thisApp.equals(othersApp)) {
                            return false;
                        }
                        return true;
                    }
    
                }
    
                private static final class Value {
    
                    private final Set<URL> searchPath;
    
                    private Value(URL... searchPath) {
                        requireNoneNull((Object) searchPath);
                        if (searchPath == null || searchPath.length == 0) {
                            this.searchPath = EMPTY_SEARCH_PATH;
                        }
                        else {
                            this.searchPath = new LinkedHashSet<>(Arrays.asList(searchPath));
                            Iterator<URL> itr = this.searchPath.iterator();
                            while (itr.hasNext()) {
                                URL searchPathEntry = itr.next();
                                String proto = searchPathEntry.getProtocol();
                                if ("file".equals(proto) || "jar".equals(proto)) {
                                    continue;
                                }
                                itr.remove();
                            }
                            verify();
                        }
                    }
    
                    private Set<URL> getSearchPath() {
                        verify();
                        return (searchPath == EMPTY_SEARCH_PATH) ? searchPath : Collections.unmodifiableSet(searchPath);
                    }
    
                    private void verify() {
                        Iterator<URL> itr = searchPath.iterator();
                        while (itr.hasNext()) {
                            try {
                                if (!Files.exists(Paths.get(itr.next().toURI()))) {
                                    itr.remove();
                                }
                            }
                            catch (IllegalArgumentException | URISyntaxException | SecurityException e) {
                                itr.remove();
                            }
                        }
                    }
    
                    @Override
                    public int hashCode() {
                        final int prime = 31;
                        int result = 1;
                        result = prime * result + ((searchPath == null) ? 0 : searchPath.hashCode());
                        return result;
                    }
    
                    @Override
                    public boolean equals(Object obj) {
                        if (this == obj) {
                            return true;
                        }
                        if (obj == null) {
                            return false;
                        }
                        if (!(obj instanceof Value)) {
                            return false;
                        }
                        Value other = (Value) obj;
                        if (searchPath == null) {
                            if (other.searchPath != null) {
                                return false;
                            }
                        }
                        else if (!searchPath.equals(other.searchPath)) {
                            return false;
                        }
                        return true;
                    }
    
                }
    
                private final Map<Key, Value> m = new LinkedHashMap<>();
                private Supplier<Application> appProvider;
    
                private ModuleRegistry() {
                }
    
                private ModuleRegistry(ModuleRegistry mr) {
                    m.putAll(mr.m);
                    appProvider = mr.appProvider;
                }
    
                private void putLibraryEntry(Library lib, LoadingMode mode, Application app, URL... searchPath) {
                    m.put(new LibraryKey(lib, mode, app), new Value(searchPath));
                }
    
                private void putApplicationEntry(Application app, URL... searchPath) {
                    m.put(new ApplicationKey(app), new Value(searchPath));
                }
    
                private Set<Library> getLibraries(Application app) {
                    Set<Library> ret = null;
                    for (Key k : m.keySet()) {
                        if (!(k instanceof LibraryKey)) {
                            continue;
                        }
                        LibraryKey lk = (LibraryKey) k;
                        if (lk.getApplication().equals(app)) {
                            if (ret == null) {
                                ret = new LinkedHashSet<>();
                            }
                            ret.add(lk.getLibrary());
                        }
                    }
                    if (ret == null) {
                        ret = NO_LIBS;
                    }
                    return ret;
                }
    
                private Set<URL> getLibrarySearchPath(Library lib, LoadingMode mode, Application app) {
                    Set<URL> ret = EMPTY_SEARCH_PATH;
                    Value v = m.get(new LibraryKey(lib, mode, app));
                    if (mode == LoadingMode.DYNAMIC && (v == null || v.getSearchPath().isEmpty())) {
                        v = m.get(new LibraryKey(lib, LoadingMode.STATIC, app));
                    }
                    if (v != null) {
                        ret = v.getSearchPath();
                    }
                    return ret;
                }
    
                private Set<URL> getApplicationSearchPath(Application app) {
                    Set<URL> ret = EMPTY_SEARCH_PATH;
                    Value v = m.get(new ApplicationKey(app));
                    if (v != null) {
                        ret = v.getSearchPath();
                    }
                    return ret;
                }
    
                private Supplier<Application> getApplicationProvider() {
                    return appProvider;
                }
    
                private void setApplicationProvider(Supplier<Application> appProvider) {
                    requireNoneNull(appProvider);
                    requireNoneNull(appProvider.get());
                    this.appProvider = appProvider;
                }
    
                private void clear() {
                    m.clear();
                }
    
            }
    
            public static final class Builder {
    
                private final ModuleRegistry registry = new ModuleRegistry();
    
                private Builder() {
                }
    
                public synchronized Builder withLibrary(Library lib, LoadingMode mode, Application app, URL... searchPath) {
                    registry.putLibraryEntry(lib, mode, app, searchPath);
                    return this;
                }
    
                public synchronized Builder withApplication(Application app, URL... searchPath) {
                    registry.putApplicationEntry(app, searchPath);
                    return this;
                }
    
                public synchronized Builder withApplicationProvider(Supplier<Application> appProvider) {
                    registry.setApplicationProvider(appProvider);
                    return this;
                }
    
                public synchronized SimpleStaticConfiguration build() {
                    SimpleStaticConfiguration ret = new SimpleStaticConfiguration(this);
                    registry.clear();
                    return ret;
                }
    
                public synchronized Builder reset() {
                    registry.clear();
                    return this;
                }
    
            }
    
            public static final Set<URL> EMPTY_SEARCH_PATH = Collections.emptySet();
            private static final Set<Library> NO_LIBS = Collections.emptySet();
    
            public static Builder newBuilder() {
                return new Builder();
            }
    
            private final ModuleRegistry registry;
    
            private SimpleStaticConfiguration(Builder b) {
                registry = new ModuleRegistry(b.registry);
            }
    
            @Override
            public Application getCurrentApplication() {
                return registry.getApplicationProvider().get();
            }
    
            @Override
            public Iterable<URL> getLibrarySearchPath(Library lib, LoadingMode mode, Application app) {
                return registry.getLibrarySearchPath(lib, mode, app);
            }
    
            @Override
            public Iterable<URL> getApplicationSearchPath(Application app) {
                return registry.getApplicationSearchPath(app);
            }
    
            @Override
            public Iterable<Library> getApplicationLibraries(Application app) {
                return registry.getLibraries(app);
            }
    
        }
    
        private static final String JAVA_HOME_PROP = System.getProperty("java.home");
    
        private static void requireNoneNull(Object... args) {
            if (args != null) {
                for (Object o : args) {
                    Objects.requireNonNull(o);
                }
            }
        }
    
        private final Lock readLock, writeLock;
        private Configuration cfg;
    
        {
            ReadWriteLock rwl = new ReentrantReadWriteLock(false);
            readLock = rwl.readLock();
            writeLock = rwl.writeLock();
        }
    
        public ConfigurableClasspathClassLoader(Configuration cfg, ClassLoader parent) {
            super(new URL[0], parent);
            setConfiguration(cfg);
        }
    
        public void setConfiguration(Configuration cfg) {
            requireNoneNull(cfg);
            try {
                writeLock.lock();
                this.cfg = cfg;
            }
            finally {
                writeLock.unlock();
            }
        }
    
        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            if (name == null) {
                throw new ClassNotFoundException(name);
            }
            synchronized (getClassLoadingLock(name)) {
                Class<?> ret;
                Class<?> self = getClass();
                if (self.getName().equals(name)) {
                    // no need to "reload" our own class
                    return self;
                }
                ret = findLoadedClass(name);
                if (ret != null) {
                    // already loaded
                    return ret;
                }
                // unknown
                ret = findClass(name);
                if (resolve) {
                    resolveClass(ret);
                }
                return ret;
            }
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            // perform a search on the global classpath (obviously far from ideal)
            Enumeration<URL> allMatches;
            String modifiedName = name.replace(".", "/").concat(".class");
            try {
                allMatches = getResources(modifiedName);
            }
            catch (IOException ioe) {
                throw new ClassNotFoundException(name);
            }
            Set<URL> filteredMatches = new LinkedHashSet<>();
            while (allMatches.hasMoreElements()) {
                URL match = allMatches.nextElement();
                if (match.getPath().replaceFirst("file:", "").startsWith(JAVA_HOME_PROP)) {
                    // probably a bootstrap classpath class - these are off limits to us
                    return getParent().loadClass(name);
                }
                // candidate match
                filteredMatches.add(match);
            }
            if (!filteredMatches.isEmpty()) {
                try {
                    readLock.lock();
                    BiFunction<Configuration.Module, Iterable<URL>, URL[]> matcher = (module, searchPath) -> {
                        URL[] ret = null;
                        if (module.includes(name)) {
                            outer: for (URL searchPathEntry : searchPath) {
                                for (URL filteredMatch : filteredMatches) {
                                    if (filteredMatch != null && filteredMatch.getPath().replaceFirst("file:", "")
                                            .startsWith(searchPathEntry.getPath())) {
                                        ret = new URL[] { filteredMatch, searchPathEntry };
                                        break outer;
                                    }
                                }
                            }
                        }
                        return ret;
                    };
                    Configuration.Application app = cfg.getCurrentApplication();
                    URL matchedClassResource = null, matchingSearchPath = null;
                    if (app != null) {
                        // try an application search path match
                        URL[] tmp = matcher.apply(app, cfg.getApplicationSearchPath(app));
                        if (tmp != null) {
                            matchedClassResource = tmp[0];
                            matchingSearchPath = tmp[1];
                        }
                        else {
                            // try matching against the search path of any library "known to" app
                            for (Configuration.Library lib : cfg.getApplicationLibraries(app)) {
                                tmp = matcher.apply(lib,
                                        cfg.getLibrarySearchPath(lib, Configuration.LoadingMode.DYNAMIC, app));
                                if (tmp != null) {
                                    matchedClassResource = tmp[0];
                                    matchingSearchPath = tmp[1];
                                    break;
                                }
                            }
                        }
                        if (matchedClassResource != null) {
                            // matched - load
                            byte[] classData = readClassData(matchedClassResource);
                            return defineClass(name, classData, 0, classData.length,
                                    constructClassDomain(matchingSearchPath));
                        }
                    }
                }
                finally {
                    readLock.unlock();
                }
            }
            throw new ClassNotFoundException(name);
        }
    
        private byte[] readClassData(URL classResource) {
            try (InputStream in = new BufferedInputStream(classResource.openStream());
                    ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                while (in.available() > 0) {
                    out.write(in.read());
                }
                return out.toByteArray();
    
            }
            catch (IOException ioe) {
                throw new RuntimeException(ioe);
            }
        }
    
        private ProtectionDomain constructClassDomain(URL codeSourceLocation) {
            CodeSource cs = new CodeSource(codeSourceLocation, (Certificate[]) null);
            return new ProtectionDomain(cs, getPermissions(cs), this, null);
        }
    
    }
    

    注意:

    • 使用加载程序注册的搜索路径必须是有效类路径的子集(子树)(&#34; java.class.path&#34;属性)。此外,&#34;脂肪JARs&#34;不受支持。
    • 由于帖子长度限制,我无法包含用法示例。如果要求,我将通过要点提供一个。