是否可以为 Java 创建自定义 class 加载程序以有条件地从 JAR 或 CLASS_PATH 加载 class?
Can a custom class loader be created for Java to conditionally load a class from JAR or from CLASS_PATH?
我们在工作中讨论了加载 Java 库的静态(来自构建的 JAR)与动态(来自 CLASS_PATH 中的单独位置)的优缺点。
在讨论的过程中,我突然想到:不管哪一方是对的,也许有一种方法是鱼与熊掌兼得:
- 为您的组织提供默认的自定义 class 加载器
- Class 加载程序 - 当加载一个特定的库时 - 检查一个配置,对于每个库(或应用程序+库组合更细粒度),包含一个标志来确定是否要静态加载库来自 JAR 或动态来自 CLASS_PATH
- 所有应用程序都在其 JAR 中使用 classes 库构建(如果您不想动态加载新的库版本,则使用默认 "backup" 版本的库)
- 如果您希望静态加载您的库(例如,因为新版本与旧版本不兼容,或者只是为了在必要时消除更改风险),请将该库的配置标志设置为 true。
- 如果您的更改风险是 zero/low,则将标志设置为 false;并允许动态加载库,从而允许您发布由所有应用程序获取的库的新版本,而无需重新编译和重新发布(显然,应用程序需要为此使用新库进行测试绝不是错误方法的雷区)。
无论我的想法是好是坏,我想知道的是,我的第二个要点在技术上是否可行Java(比如 1.8+),如果是的话,实施它会涉及什么?
是的,绝对是。我将假设 Java 8 来回答这个问题,因为我还没有熟悉 Java 9 的 Module System.
的功能
主要思想很简单——一个 ClassLoader
的 loadClass(String, boolean)
实现遵循以下 "protocol":
- 如果 class 名称参数引用系统 class,则将加载委托给您的父加载器和 return。否则继续。
- 根据您的配置,该名称是否引用应用程序 class?如果是,请继续执行步骤 2.1,否则转到 3。
- 获取应用程序"search path"(JAR文件本身的文件系统路径根据问题中的要求)
- 尝试以特定于实现的方式(例如通过针对搜索路径条目的路径解析)在搜索路径下与名称参数匹配的 class 资源。如果存在这样的资源,则继续执行步骤 5,否则执行步骤 3。
- 对于每个尚未检查的已知库,按优先级降序排列:
- 该名称是否指的是该图书馆的 class?如果是这样继续;否则 return 到 3.
- 获取特定库的搜索路径。
- 尝试在与名称参数匹配的搜索路径下找到 class 资源。如果成功,则继续执行第 5 步,否则 return 执行第 3 步。
- 如果在步骤 2 或 3 中没有建立匹配的 class 资源,则引发异常。否则继续。
- 检索资源内容,酌情委托给
defineClass
和resolveClass
,return新class.
下面是这样一个 ClassLoader
. 的(非常丑陋的)示例实现
假设:
- 一个模块是一个应用程序或一个库。
- 一个模块包含一组已知的classes;换句话说,一个模块 "is aware of its contents".
- 一个应用程序使用零个或多个库。
- 多个逻辑应用程序可以 运行 在同一个 JVM 上。
- A
Configuration
与其 ClassLoader
通信:
- "current" 应用程序请求 class 加载。
- 应用程序和搜索路径之间的映射。
- 库-应用程序对和搜索路径之间的映射。
- 多个加载器可以使用相同的配置(的持久表示)。
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);
}
}
备注:
- 加载器注册的搜索路径必须是有效class路径("java.class.path"属性)的子集(子树)。此外,不支持 "fat JARs"。
- 由于 post 长度限制,我无法包含用法示例。如果需要,我会通过 gist 提供一个。
我们在工作中讨论了加载 Java 库的静态(来自构建的 JAR)与动态(来自 CLASS_PATH 中的单独位置)的优缺点。
在讨论的过程中,我突然想到:不管哪一方是对的,也许有一种方法是鱼与熊掌兼得:
- 为您的组织提供默认的自定义 class 加载器
- Class 加载程序 - 当加载一个特定的库时 - 检查一个配置,对于每个库(或应用程序+库组合更细粒度),包含一个标志来确定是否要静态加载库来自 JAR 或动态来自 CLASS_PATH
- 所有应用程序都在其 JAR 中使用 classes 库构建(如果您不想动态加载新的库版本,则使用默认 "backup" 版本的库)
- 如果您希望静态加载您的库(例如,因为新版本与旧版本不兼容,或者只是为了在必要时消除更改风险),请将该库的配置标志设置为 true。
- 如果您的更改风险是 zero/low,则将标志设置为 false;并允许动态加载库,从而允许您发布由所有应用程序获取的库的新版本,而无需重新编译和重新发布(显然,应用程序需要为此使用新库进行测试绝不是错误方法的雷区)。
无论我的想法是好是坏,我想知道的是,我的第二个要点在技术上是否可行Java(比如 1.8+),如果是的话,实施它会涉及什么?
是的,绝对是。我将假设 Java 8 来回答这个问题,因为我还没有熟悉 Java 9 的 Module System.
的功能主要思想很简单——一个 ClassLoader
的 loadClass(String, boolean)
实现遵循以下 "protocol":
- 如果 class 名称参数引用系统 class,则将加载委托给您的父加载器和 return。否则继续。
- 根据您的配置,该名称是否引用应用程序 class?如果是,请继续执行步骤 2.1,否则转到 3。
- 获取应用程序"search path"(JAR文件本身的文件系统路径根据问题中的要求)
- 尝试以特定于实现的方式(例如通过针对搜索路径条目的路径解析)在搜索路径下与名称参数匹配的 class 资源。如果存在这样的资源,则继续执行步骤 5,否则执行步骤 3。
- 对于每个尚未检查的已知库,按优先级降序排列:
- 该名称是否指的是该图书馆的 class?如果是这样继续;否则 return 到 3.
- 获取特定库的搜索路径。
- 尝试在与名称参数匹配的搜索路径下找到 class 资源。如果成功,则继续执行第 5 步,否则 return 执行第 3 步。
- 如果在步骤 2 或 3 中没有建立匹配的 class 资源,则引发异常。否则继续。
- 检索资源内容,酌情委托给
defineClass
和resolveClass
,return新class.
下面是这样一个
ClassLoader
. 的(非常丑陋的)示例实现
假设:
- 一个模块是一个应用程序或一个库。
- 一个模块包含一组已知的classes;换句话说,一个模块 "is aware of its contents".
- 一个应用程序使用零个或多个库。
- 多个逻辑应用程序可以 运行 在同一个 JVM 上。
- A
Configuration
与其ClassLoader
通信:- "current" 应用程序请求 class 加载。
- 应用程序和搜索路径之间的映射。
- 库-应用程序对和搜索路径之间的映射。
- 多个加载器可以使用相同的配置(的持久表示)。
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);
}
}
备注:
- 加载器注册的搜索路径必须是有效class路径("java.class.path"属性)的子集(子树)。此外,不支持 "fat JARs"。
- 由于 post 长度限制,我无法包含用法示例。如果需要,我会通过 gist 提供一个。