使用 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." 或者也许您可以,但是解决方案很难(换句话说,维护和维护成本很高)容易折断。

在这种情况下,最好知道为什么您不能更改具有明显和非常严重的设计问题的软件。因为这才是真正的问题。