8

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();
flakes
  • 21,558
  • 8
  • 41
  • 88
  • 2
    You'd have to isolate/duplicate whole class hierarchies in their own classloaders, and before you even go down that way there are plenty of easier possibilities to look at. But for us to consider those possibilities you'll have to explain what that class does, how it's used, and what your root problem is (not how you thought about solving it). – Kayaman Apr 20 '18 at 06:58
  • @Kayaman Root of the problem is that the legacy code base doesn't provide a good way of inject configurations. They're strongly tied to the classes because of the singleton pattern. Without a major rewrite of the code base there's no real way to allow multiple configurations for many classes. This is probably not the best solution, but I'm curious what this solution would require (as an educational exercise at this point). My best option in reality is probably containerizing the application/environment and running several instances of the application. – flakes Apr 20 '18 at 07:05
  • It wouldn't be enough to load just the singleton class dynamically. You'd have to load the hierarchy, which would result in something like containerization. But that would probably be taking the hard way. Hard to say without knowing the specifics. – Kayaman Apr 20 '18 at 07:13
  • @Kayaman I see; that makes sense. One advantage I was curious of were potential savings in program memory by not having to launch a multiple JVM instances. But if I have to reload everything in the classpath then I probably don't save much of anything, and end up increasing the complexity of the application a lot. That said; I am still curious what this implementation would look like. I couldn't find any example to run off of, but I may just not know the right buzzwords to search for. – flakes Apr 20 '18 at 07:28
  • 1
    You need to show us how you make a decision that the flow show take Path A or B and also how the request is being handled, what framework is used? – Tarun Lalwani Apr 22 '18 at 13:06
  • @Tarun I'm looking for an answer which is framework agnostic. What I want to know is how I can spin up two copies of the `Handler` class (or any other intermediary classes. ie some class `Foo` that uses `Bar` which uses `Singleton`) such that they have reference to different copies of the `Singleton` class. The decision on which `Handler` to use after class creation is not necessarily in scope for this question – flakes Apr 22 '18 at 20:08
  • I can think of solutions but I need to understand few things. Is this the common handler which does the `handleRequest`? If you have a way to write two handlers which can do Path A and Path B, can you do that? If not, then can you take the decision of `true` or `false` based on the stack trace. Then one can override the singleton and the singleton class can decided what value to return based on stack trace or something else like current request, that is why I was asking about the framework used – Tarun Lalwani Apr 23 '18 at 04:26
  • @TarunLalwani 1) No. There are a few different types of handlers and other classes which reference the `Singleton`. I might need to repeat this pattern a few different times. I imagine having a factory or cache for classes generated with isolated `Singleton`.. probably binding the instances in some dependency injection framework. 2) The bool example is probably too simplified. This object has many dimensions. I could re-write the handler logic, but it would take a lot of effort to do so; for sake of this question lets say no. 3) I don't have the ability to change the logic of the singleton. – flakes Apr 23 '18 at 06:18
  • I would like to see some parts of the actual code to give a sensible option, it is not that there are many but given no knowledge of the context, it really to hard to just show some approach. Please share what you can from one such class code – Tarun Lalwani Apr 23 '18 at 21:10

3 Answers3

3

/First, tell your lead that you are about spend time and make convoluted code that uses more memory and could be slower due to jit re-compilation and other class init issues, because you are somehow not allowed to fix bad obsolete code. Time and maintainability is money./

Ok, now... Your funky classloaders are just URL classloaders with the required jars given. But the trick is to NOT have the singleton nor the handlers in the main classpath, otherise the classloader will find the class in a parentclassloader (that has precedence) and it will still be a singleton. You knew that I'm sure.

Another radical solution is to re-implement the offending class your way (with properties file, or with a Threadlocal field (assuming the work is done on same thread) that a caller can set before making the call to the handler, which in turn will not see the mascarade).

You would have to deploy your overriding class with precedence in the classpath (list the jar earlier), or for a webapp, if you can, deploy in the web-inf/classes which overrides anything in the web-inf/lib. You can ultimately just delete the class from the legacy jar. The point is to have the same classname, same methods signature, but a new implementation (again, that relies on loading cfg files or using threadlocal setup before the call).

Hope this helps.

user2023577
  • 1,752
  • 1
  • 12
  • 23
3

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.

sujit
  • 2,258
  • 1
  • 15
  • 24
2

In my view –and my apologies that this is a largely opinion-based answer– this is a business problem and not a technical problem, because the constraints given ("I cannot change the code") are not technical ones. But as anybody working in software development can attest, business constraints are part and parcel of our jobs.

Your issue can be abstracted as follows: "Considering constraint A, can I get result B?" And the answer is: "No, you cannot." Or perhaps you can, but with a solution that is difficult –in other words, expensive– to maintain and prone to break.

In cases like these, it would be good to know why you cannot change the software that has obvious and very serious design problems. Because that is the real problem.

SeverityOne
  • 2,476
  • 12
  • 25