-3

I'm loading in classes from a JAR that implement an interface from a public API. The interface itself will remain constant but other classes associated with the API may change over time. Clearly once the API changes we will no longer be able to support implementations of the interface that were written with the old version. However some of the interface methods provide simple meta-data of type String that we can assume will never change and never rely on the other parts of the API that may change. I would like to be able to extract this meta-data even when the API has changed.

For example consider the following implementation that might be loaded in where Foo is the interface and Bar is an another class in the API. I want to call the name method even when the class Bar no longer exists.

class MyFoo implements Foo {
   Bar bar = null;

   @Override public String name() {
      return "MyFoo"
   }
}

As far as I can see the obvious approach is to override loadClass(String name) in my custom ClassLoader and return some "fake" class for Bar. The meta-data methods can be assumed to never create or use a Bar object. The question is how to generate this "fake" class when asked to load Bar. I've thought about the following approaches:

  1. Simply return any old existing class. I've tried returning Object.class but this still results in a NoClassDefFoundError for Bar when I try to instantiate an instance of Foo.
  2. Use ASM to generate the byte code for a new class from scratch.
  3. Use ASM to rename some sort of empty template class to match Bar and load that.

Both 2. and 3. seem quite involved, so I was wondering if there was an easier way to achieve my goal?

user2667523
  • 190
  • 1
  • 9

1 Answers1

2

Here is a class loader which will create a dummy class for every class it didn’t find on the search path, in a very simple way:

public class DummyGeneratorLoader extends URLClassLoader {

    public DummyGeneratorLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public DummyGeneratorLoader(URL[] urls) {
        super(urls);
    }

    public DummyGeneratorLoader(
        URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }

    static final byte[] template = ("Êþº¾\0\0\0002\0\n\1\7\0\1\1\0\20java/lang/Object"
        + "\7\0\3\1\0\6<init>\1\0\3()V\14\0\5\0\6\n\0\4\0\7\1\0\4Code\0\1\0\2\0\4\0"
        + "\0\0\0\0\1\0\1\0\5\0\6\0\1\0\t\0\0\0\21\0\1\0\1\0\0\0\5*·\0\b±\0\0\0\0\0\0")
        .getBytes(StandardCharsets.ISO_8859_1);

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            return super.findClass(name);
        }
        catch(ClassNotFoundException ex) { }
        return new ByteArrayOutputStream(template.length + name.length() + 10) { {
            write(template, 0, 11);
            try { new DataOutputStream(this).writeUTF(name.replace('.', '/')); }
            catch (IOException ex) { throw new AssertionError(); }
            write(template, 11, template.length - 11);
        }
        Class<?> toClass(String name) {
            return defineClass(name, buf, 0, count); } }.toClass(name);
    }
}

However, there might be a lot of expectations or structural constraints imposed by the using code which the dummy class can’t fulfill. After all, before you can invoke the interface method, you have to create an instance of the class, so it has to pass verification and a successful execution of its constructor.

If the methods truly have the assumed structure like public String name() { return "MyFoo"; } using ASM may be the simpler choice, but not to generate an arbitrarily complex fake environment, but to parse these methods and predict the constant value they’d return. Such a method would consist of two instructions only, ldc value and areturn. You only need to check that this is the case and extract the value from the first instruction.

Holger
  • 285,553
  • 42
  • 434
  • 765