使用 class 加载程序隔离静态单例 class
Isolating a static singleton class using class loaders
免责声明:鉴于此问题,这可能不是最佳解决方案,但我很好奇如何实现此实现。
问题 我正在尝试处理一些遗留代码,这些代码具有如下定义的单例:
public class LegacySingleton {
private static Boolean value;
public static void setup(boolean v) {
if (value != null) {
throw new RuntimeException("Already Set up");
}
value = v;
System.out.println("Setup complete");
}
public static void teardown() {
value = null;
System.out.println("Teardown complete");
}
public static boolean getValue() {
return value;
}
}
我没有能力更改此设计,并且 class 在整个代码库中被大量使用。此单例返回的值可以极大地改变代码的功能。例如:
public class LegacyRequestHandler {
public void handleRequest() {
if (LegacySingleton.getValue()) {
System.out.println("Path A");
} else {
System.out.println("Path B");
}
}
}
现在如果我想让代码采用 Path A
,那么我必须以特定方式初始化 LegacySingleton
。如果我想使用 Path B
,我必须重新初始化 LegacySingleton
。无法并行处理采用不同路径的请求; LegacySingleton
的每个不同配置意味着我需要启动一个单独的 JVM 实例。
我的问题 是否可以使用单独的 class 加载程序来隔离此单例?我一直在玩 ClassLoader
API,但我不太明白。
我想它看起来应该是这样的:
public class LegacyRequestHandlerProvider extends Supplier<LegacyRequestHandler> {
private final boolean value;
public LegacyRequestHandlerProvider(boolean value) {
this.value = value;
}
@Override
public LegacyRequestHandler get() {
LegacySingleton.setup(value);
return new LegacyRequestHandler();
}
}
...
ClassLoader loader1 = new SomeFunkyClassLoaderMagic();
Supplier<LegacyRequestHandler> supplier1 = loader1
.loadClass("com.project.LegacyRequestHandlerProvider")
.getConstructor(Boolean.TYPE)
.newInstance(true);
ClassLoader loader2 = new SomeFunkyClassLoaderMagic();
Supplier<LegacyRequestHandler> supplier2 = loader2
.loadClass("com.project.LegacyRequestHandlerProvider")
.getConstructor(Boolean.TYPE)
.newInstance(false);
LegacyRequestHandler handler1 = supplier1.get();
LegacyRequestHandler handler2 = supplier2.get();
/首先,告诉您的领导,您将花费时间制作复杂的代码,这些代码会使用更多内存并且由于 jit 重新编译和其他 class 初始化问题可能会变慢,因为以某种方式不允许您修复错误的过时代码。时间和可维护性就是金钱。/
好的,现在...您时髦的 classloader 只是 URL classloader 提供了所需的 jars。但诀窍是在主 class 路径中不要有单例或处理程序,否则 classloader 会在父 classloader 中找到 class(有优先级)并且它仍然是一个单例。你知道我确定。
另一个激进的解决方案是重新实现有问题的 class 调用者可以在进行调用处理程序,而处理程序将看不到伪装)。
您必须在 class 路径中优先部署您的重写 class(较早列出 jar),或者对于网络应用程序,如果可以的话,部署在 web-inf/classes 覆盖 web-inf/lib 中的任何内容。您最终可以从遗留 jar 中删除 class。关键是要有相同的 class 名称、相同的方法签名,但有一个新的实现(同样,这依赖于加载 cfg 文件或在调用之前使用线程本地设置)。
希望这对您有所帮助。
在您希望 getValue
的输出发生变化的特定时间点,简单反射是否有效?
Field f = LegacySingleton.class.getDeclaredField("value");
f.setAccessible(true);
f.set(null, true|false);
如果没有,对于Classloader的方式,可以遵循Plugin架构。但正如其他人所指出的,这可能归结为在 2 个不同的类加载器层次结构上加载的整个依赖项。此外,您可能会遇到 LinkageError
问题,具体取决于依赖项在您的代码库中的工作方式。
受此启发post:
- 隔离代码库,主要是具有
LegacyRequestHandler
class 且不包含在 application/main class 路径中的 jar。
- 有一个自定义的 classloader 首先向内看,然后检查父级。这种 classloader 的完整示例是 here.
有一个包装器调用程序,它将使用提供 LegacySingleton
class 的 jar 路径初始化 class 加载程序,例如
new ParentLastURLClassLoader(Arrays.asList(new URL[] {new URL("path/to/jar")}));
Post 您可以在其 class 加载器 space 中加载单例并获取副本。
//2 different classloaders
ClassLoader cl1 = new ParentLastURLClassLoader(urls);
ClassLoader cl2 = new ParentLastURLClassLoader(urls);
//LegacySingleton with value = true in Classloader space of cl1
cl1.loadClass("LegacySingleton").getMethod("setup", boolean.class).invoke(null, true);
//LegacySingleton with value = false in Classloader space of cl1
cl2.loadClass("LegacySingleton").getMethod("setup", boolean.class).invoke(null, false);
- 接下来,您可以使用反射(通过
cl1/2
)获取遗留代码的驱动程序class并触发执行。
注意 你不应该直接在主 class 中引用遗留代码中的 classes,因为它们将被加载使用 Java primordial/application class 加载程序。
在我看来——我很抱歉这主要是基于意见的回答——这是一个业务问题而不是技术问题,因为给定的限制 ("I cannot change the code") 不是技术问题。但是,任何从事软件开发工作的人都可以证明,业务限制是我们工作的一部分。
您的问题可以抽象如下:"Considering constraint A, can I get result B?" 答案是:"No, you cannot." 或者也许您可以,但是解决方案很难(换句话说,维护和维护成本很高)容易折断。
在这种情况下,最好知道为什么您不能更改具有明显和非常严重的设计问题的软件。因为这才是真正的问题。
免责声明:鉴于此问题,这可能不是最佳解决方案,但我很好奇如何实现此实现。
问题 我正在尝试处理一些遗留代码,这些代码具有如下定义的单例:
public class LegacySingleton {
private static Boolean value;
public static void setup(boolean v) {
if (value != null) {
throw new RuntimeException("Already Set up");
}
value = v;
System.out.println("Setup complete");
}
public static void teardown() {
value = null;
System.out.println("Teardown complete");
}
public static boolean getValue() {
return value;
}
}
我没有能力更改此设计,并且 class 在整个代码库中被大量使用。此单例返回的值可以极大地改变代码的功能。例如:
public class LegacyRequestHandler {
public void handleRequest() {
if (LegacySingleton.getValue()) {
System.out.println("Path A");
} else {
System.out.println("Path B");
}
}
}
现在如果我想让代码采用 Path A
,那么我必须以特定方式初始化 LegacySingleton
。如果我想使用 Path B
,我必须重新初始化 LegacySingleton
。无法并行处理采用不同路径的请求; LegacySingleton
的每个不同配置意味着我需要启动一个单独的 JVM 实例。
我的问题 是否可以使用单独的 class 加载程序来隔离此单例?我一直在玩 ClassLoader
API,但我不太明白。
我想它看起来应该是这样的:
public class LegacyRequestHandlerProvider extends Supplier<LegacyRequestHandler> {
private final boolean value;
public LegacyRequestHandlerProvider(boolean value) {
this.value = value;
}
@Override
public LegacyRequestHandler get() {
LegacySingleton.setup(value);
return new LegacyRequestHandler();
}
}
...
ClassLoader loader1 = new SomeFunkyClassLoaderMagic();
Supplier<LegacyRequestHandler> supplier1 = loader1
.loadClass("com.project.LegacyRequestHandlerProvider")
.getConstructor(Boolean.TYPE)
.newInstance(true);
ClassLoader loader2 = new SomeFunkyClassLoaderMagic();
Supplier<LegacyRequestHandler> supplier2 = loader2
.loadClass("com.project.LegacyRequestHandlerProvider")
.getConstructor(Boolean.TYPE)
.newInstance(false);
LegacyRequestHandler handler1 = supplier1.get();
LegacyRequestHandler handler2 = supplier2.get();
/首先,告诉您的领导,您将花费时间制作复杂的代码,这些代码会使用更多内存并且由于 jit 重新编译和其他 class 初始化问题可能会变慢,因为以某种方式不允许您修复错误的过时代码。时间和可维护性就是金钱。/
好的,现在...您时髦的 classloader 只是 URL classloader 提供了所需的 jars。但诀窍是在主 class 路径中不要有单例或处理程序,否则 classloader 会在父 classloader 中找到 class(有优先级)并且它仍然是一个单例。你知道我确定。
另一个激进的解决方案是重新实现有问题的 class 调用者可以在进行调用处理程序,而处理程序将看不到伪装)。
您必须在 class 路径中优先部署您的重写 class(较早列出 jar),或者对于网络应用程序,如果可以的话,部署在 web-inf/classes 覆盖 web-inf/lib 中的任何内容。您最终可以从遗留 jar 中删除 class。关键是要有相同的 class 名称、相同的方法签名,但有一个新的实现(同样,这依赖于加载 cfg 文件或在调用之前使用线程本地设置)。
希望这对您有所帮助。
在您希望 getValue
的输出发生变化的特定时间点,简单反射是否有效?
Field f = LegacySingleton.class.getDeclaredField("value");
f.setAccessible(true);
f.set(null, true|false);
如果没有,对于Classloader的方式,可以遵循Plugin架构。但正如其他人所指出的,这可能归结为在 2 个不同的类加载器层次结构上加载的整个依赖项。此外,您可能会遇到 LinkageError
问题,具体取决于依赖项在您的代码库中的工作方式。
受此启发post:
- 隔离代码库,主要是具有
LegacyRequestHandler
class 且不包含在 application/main class 路径中的 jar。 - 有一个自定义的 classloader 首先向内看,然后检查父级。这种 classloader 的完整示例是 here.
有一个包装器调用程序,它将使用提供
LegacySingleton
class 的 jar 路径初始化 class 加载程序,例如new ParentLastURLClassLoader(Arrays.asList(new URL[] {new URL("path/to/jar")}));
Post 您可以在其 class 加载器 space 中加载单例并获取副本。
//2 different classloaders
ClassLoader cl1 = new ParentLastURLClassLoader(urls);
ClassLoader cl2 = new ParentLastURLClassLoader(urls);
//LegacySingleton with value = true in Classloader space of cl1
cl1.loadClass("LegacySingleton").getMethod("setup", boolean.class).invoke(null, true);
//LegacySingleton with value = false in Classloader space of cl1
cl2.loadClass("LegacySingleton").getMethod("setup", boolean.class).invoke(null, false);
- 接下来,您可以使用反射(通过
cl1/2
)获取遗留代码的驱动程序class并触发执行。
注意 你不应该直接在主 class 中引用遗留代码中的 classes,因为它们将被加载使用 Java primordial/application class 加载程序。
在我看来——我很抱歉这主要是基于意见的回答——这是一个业务问题而不是技术问题,因为给定的限制 ("I cannot change the code") 不是技术问题。但是,任何从事软件开发工作的人都可以证明,业务限制是我们工作的一部分。
您的问题可以抽象如下:"Considering constraint A, can I get result B?" 答案是:"No, you cannot." 或者也许您可以,但是解决方案很难(换句话说,维护和维护成本很高)容易折断。
在这种情况下,最好知道为什么您不能更改具有明显和非常严重的设计问题的软件。因为这才是真正的问题。