4
\$\begingroup\$

I needed to dynamically load and instantiate some classes from a generated jar and a generated .class file which depends on that jar. So I came up with this:

package ytm.yajco.translation.util;

import org.jetbrains.annotations.NotNull;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;

import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;

public class DynamicClass {
    protected static final StackWalker walker = StackWalker.getInstance(RETAIN_CLASS_REFERENCE);
    protected final        Class<?>    loadedClass;

    /**
     * <b>WARNING:</b> Do not call this constructor if {@code jar} has already been loaded.
     * This will avoid multiple class loading.
     * <p>
     * Creates a new class loader which can load classes from {@code jar}.
     * The new classloader inherits the classpath from the class loader of the caller of this constructor.
     * Finally, the sought class is loaded from the given {@code jar}.
     *
     * @param classFQN fully qualified name of the class to be loaded
     * @param jar      path to the jar or root directory where {@code classFQN} is to be found
     * @see StackWalker#getCallerClass()
     */
    public DynamicClass(String classFQN, @NotNull Path jar) throws MalformedURLException, ReflectiveOperationException {
        this(classFQN, jar, walker.getCallerClass().getClassLoader());
    }

    /**
     * <b>WARNING:</b> Do not call this constructor if {@code jar} has already been loaded by {@code parentCL}
     * or its ancestors. This will avoid multiple class loading.
     * <p>
     * Creates a new class loader which can load classes from {@code jar}.
     * The new classloader inherits the classpath from {@code parentCL}.
     * Finally, the sought class is loaded from the given {@code jar}.
     *
     * @param classFQN fully qualified name of the class to be loaded
     * @param jar      path to the jar or root directory where {@code classFQN} is to be found
     * @param parentCL classloader to use as a parent
     */
    public DynamicClass(String classFQN, @NotNull Path jar, ClassLoader parentCL) throws MalformedURLException, ClassNotFoundException {
        final URL[] jarLocation = {jar.toUri().toURL()};
        final URLClassLoader classLoader = URLClassLoader.newInstance(jarLocation, parentCL);
        loadedClass = Class.forName(classFQN, true, classLoader);
    }

    /**
     * Finds the sought class using the given class loader.
     *
     * @param classFQN    fully qualified name of the class to be loaded
     * @param classLoader class loader to reuse
     * @throws ClassNotFoundException if the class cannot be located by the specified class loader
     */
    public DynamicClass(String classFQN, ClassLoader classLoader) throws ClassNotFoundException {
        loadedClass = Class.forName(classFQN, true, classLoader);
    }

    public Class<?> getLoadedClass() {
        return loadedClass;
    }

    public ClassLoader getClassLoader() {
        return loadedClass.getClassLoader();
    }
}

In one of the cases I know the interface statically so in my DynamicSupplier subclass:

public Supplier<?> newSupplierInstance() throws ReflectiveOperationException {
    return (Supplier<?>) loadedClass.getDeclaredConstructor().newInstance();
}

In the other case I know the method signatures, so I have provided methods to which hide the reflective access. Also I wrap the new instance in a small class just to put an abstract layer when working with a bare Object.

package ytm.yajco.translation.language;

import org.jetbrains.annotations.NotNull;
import ytm.yajco.translation.util.DynamicClass;

import java.io.Reader;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.nio.file.Path;

public class DynamicParser extends DynamicClass {

// skipped JavaDoc for brevity in this review
    public DynamicParser(String parserFQN, @NotNull Path parserJar) throws MalformedURLException, ReflectiveOperationException {
        this(parserFQN, parserJar, walker.getCallerClass().getClassLoader());
    }

// skipped other constructors matching super for brevity in this review


    public static Object parseSentence(@NotNull Object parser, String sentence) throws ReflectiveOperationException {
        final Method parse = parser.getClass().getMethod("parse", String.class);
        return parse.invoke(parser, sentence);
    }

    public static Object parseSentence(@NotNull Object parser, Reader sentence) throws ReflectiveOperationException {
        final Method parse = parser.getClass().getMethod("parse", Reader.class);
        return parse.invoke(parser, sentence);
    }


    public Parser newParserInstance() throws ReflectiveOperationException {
        final Object parser = loadedClass.getDeclaredConstructor().newInstance();
        return new Parser(parser);
    }


    public static class Parser {
        private final Object parser;

        private Parser(Object parser) {
            this.parser = parser;
        }

        public Object parseSentence(String sentence) throws ReflectiveOperationException {
            return DynamicParser.parseSentence(parser, sentence);
        }

        public Object parseSentence(Reader sentence) throws ReflectiveOperationException {
            return DynamicParser.parseSentence(parser, sentence);
        }

        public Object getRealParser() {
            return parser;
        }
    }

}

So what do you think about my creation? :) BTW, is Parser an adapter (as per GOF)?

\$\endgroup\$

1 Answer 1

2
+50
\$\begingroup\$

First of all, the DynamicClass wraps a whole class loader for a single class. If you need multiple classes from the same jar, you will have to create a complete class loader and read the complete jar for every single class. This seems overkill, but may well be OK in your context.

Apart from that there's not much to say, as you basically create a wrapper around three lines of code.

Regarding DynamicParser: here it gets ugly. So you know that you have two methods called parse() one with a string parameter and one with a reader. Why don't these classes simply implement an interface which encompasses parse(String and parse(Reader)? I see reflection as a last resort when all else fails, and in this case, simply implementing an interface seems much easier and much clearer.

I don't know whether you only need this for a kind of "pluggable parser" or if you have multiple locations in your code where you need this code, but from what I see, I'd do the following:

1 - Create an interface Parser which declares the methods as above

2 - Create a utility class, which can load such a parser from a jar

public static Parser loadParser(String fqn, Path jar) {
    // basically do the same as DynamicClass,
    // but upcast to parser and return an instance
}

3 - Use the Parser (as a strongly typed interface) in the client code


Edit following the comment: I did not miss the third constructor, it just did not make any sense for me.

So you have multiple uses like this:

DynamicClass dc1 = new DynamicClass("Thingy1", myJar);
...
DynamicClass dc2 = new DynamicClass("Thingy2", dc1.getClassLoader());

Where the locations are not necessarily related.

If someone inserts another call somewhere else in the code, which coincidentally is somewhere before dc1, he will have to:

DynamicClass dc0 = new DynamicClass("Thingy0", myJar);

... and then change the original call to

DynamicClass dc1 = new DynamicClass("Thingy1", dc0.getClassLoader());

In my world this is inacceptable. If you do caching, do it in the class itself, maybe by a static map or so, but keep it out of the public interface.

Regarding the interface: I feel like close-voting the question for missing review context now.

Nevertheless, I cannot see any reason why you use two different classes for DynamicParser and Parser. Just have DynamicParser implement an interface Parser and be done.

\$\endgroup\$
2
  • \$\begingroup\$ In your first point (load multiple classes from one jar) you missed the third ctor which allows to reuse a classloader - which you can get from a previous DynamicClass by a getter. ... The real parser is a generated class (by another tool), which has no outside dependencies currently and implements no interface. That means that even the exception it throws is generated and declared in the same package. I know the signature of it, but I cannot change it, I cannot get hold of a common interface. If I could, I would downcast in a subclass, same as with the Supplier. \$\endgroup\$
    – Hawk
    Commented Aug 18, 2020 at 14:06
  • \$\begingroup\$ @Hawk See edit. \$\endgroup\$
    – mtj
    Commented Aug 19, 2020 at 4:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.