Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Isolating a static singleton class using class loaders

Disclaimer: This is probably not the best solution given the issue, but I'm curious how this implementation could be achieved.

Problem I'm trying to deal with some legacy code which has a singleton defined like bellow:

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;
    }
}

I do not have the ability to change this design and the class is used heavily throughout the code base. The values returned by this singleton can greatly change the functionality of the code. Eg:

public class LegacyRequestHandler {
    public void handleRequest() {
        if (LegacySingleton.getValue()) {
            System.out.println("Path A");
        } else {
            System.out.println("Path B");
        }
    }
}

Right now if I want the code to take Path A, then I have to initialize LegacySingleton in a particular way. If I then want to take Path B I have to re-initialize the LegacySingleton. There is no way of handling requests in parallel which take different paths; meaning for each different configuration of LegacySingleton required I need to launch a separate JVM instance.


My Question Is it possible to isolate this singleton using separate class loaders? I've been playing around with the ClassLoader API, but I cant quite figure it out.

I'm imagining it would look something along the lines of this:

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();
like image 652
flakes Avatar asked Apr 20 '18 06:04

flakes


1 Answers

Does plain simple reflection work at specific points in time, where you want the output of getValue to change?

Field f = LegacySingleton.class.getDeclaredField("value");
f.setAccessible(true);
f.set(null, true|false);

If not, For the Classloader approach, you can follow a Plugin architecture. But as others have noted, this may boil down to the whole dependencies being loaded on 2 different Classloader hierarchies. Also, you may face LinkageError issues, depending on how the dependencies work in your codebase.

Inspired by this post:

  • Isolate the codebase, primarily the jar having LegacyRequestHandler class and do not include in the application/main classpath.
  • Have a custom classloader that looks inwards first and then checks parent. A complete example of such a classloader is here.
  • Have a wrapper invoker that will initialise the class loader with the jar path providing LegacySingleton class, e.g.

    new ParentLastURLClassLoader(Arrays.asList(new URL[] {new URL("path/to/jar")}));

  • Post that, you can load the singleton in it's class loader space and obtain copies.


    //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);
  • Next, you may obtain the driver class for your legacy code using reflection(through cl1/2) and trigger execution.

NOTE that you shouldn't refer to the classes in the Legacy code directly in the main class, as then they will be loaded using Java primordial/application class loader.

like image 190
sujit Avatar answered Oct 26 '22 22:10

sujit