2

I have a class with methods that return a class only if that class exists, calling the method would generate a NoClassDefFoundError caused by ClassNotFoundException exception if the classpath is not loaded (rightfully so, because it's not mandatory).

How can I still get the methods from Class#getMethods() when some of the methods (or fields) may not be loaded in the classpath? Is it possible to ignore the methods/fields when retrieving them?

Note that using try-catch will prevent the entire process from working at all, so it is NOT the solution!

Here is how I am loading the class:

try {
    String name = StringUtil.format("Nifty{0}", (isBungee ? "Bungee" : "Bukkit"));
    Reflection main = new Reflection(name, StringUtil.format("net.netcoding.{0}", name.toLowerCase())); // net.netcoding.niftybukkit.NiftyBukkit
    Object mainObj = main.invokeMethod("getPlugin", null); // See below
    logger = (Logger)main.invokeMethod("getLogger", mainObj);
} catch (Exception ex) { }

Here is the two bits that try to locate the method in Reflection:

public class Reflection {

    private static final transient ConcurrentHashMap<Class<?>, Class<?>> CORRESPONDING_TYPES = new ConcurrentHashMap<>();
    private final String className;
    private final String subPackage;
    private final String packagePath;

    static {
        CORRESPONDING_TYPES.put(Byte.class, byte.class);
        CORRESPONDING_TYPES.put(Short.class, short.class);
        CORRESPONDING_TYPES.put(Integer.class, int.class);
        CORRESPONDING_TYPES.put(Long.class, long.class);
        CORRESPONDING_TYPES.put(Character.class, char.class);
        CORRESPONDING_TYPES.put(Float.class, float.class);
        CORRESPONDING_TYPES.put(Double.class, double.class);
        CORRESPONDING_TYPES.put(Boolean.class, boolean.class);
    }

    public Reflection(String className, String packagePath) {
        this(className, "", packagePath);
    }

    public Reflection(String className, String subPackage, String packagePath) {
        this.className = className;
        this.subPackage = StringUtil.stripNull(subPackage).replaceAll("\\.$", "").replaceAll("^\\.", "");
        this.packagePath = packagePath;
    }

    public String getClassName() {
        return this.className;
    }

    public String getClassPath() {
        return this.getPackagePath() + (StringUtil.notEmpty(this.subPackage) ? "." + this.subPackage : "") + "." + this.getClassName();
    }

    public Class<?> getClazz() throws Exception {
        return Class.forName(this.getClassPath());
    }

    public Object invokeMethod(String name, Object obj, Object... args) throws Exception {
        return this.getMethod(name, toPrimitiveTypeArray(args)).invoke(obj, args);
    }

    private static Class<?> getPrimitiveType(Class<?> clazz) {
        return clazz != null ? CORRESPONDING_TYPES.containsKey(clazz) ? CORRESPONDING_TYPES.get(clazz) : clazz : null;
    }

    public Method getMethod(String name, Class<?>... paramTypes) throws Exception {
        Class<?>[] types = toPrimitiveTypeArray(paramTypes);

        // In this example, this.getClazz() simply returns
        // a Class of net.netcoding.niftybukkit.NiftyBukkit
        // this.getClazz().getMethods() throws the ClassNotFOundException
        // I want to still access the methods, even if one of them is not available
        for (Method method : this.getClazz().getMethods()) {
            Class<?>[] methodTypes = toPrimitiveTypeArray(method.getParameterTypes());

            if (method.getName().equals(name) && isEqualsTypeArray(methodTypes, types)) {
                method.setAccessible(true);
                return method;
            }
        }

        System.out.println(StringUtil.format("The method {0} was not found with parameters {1}!", name, Arrays.asList(types)));
        return null;
    }
Nahydrin
  • 13,197
  • 12
  • 59
  • 101
  • 3
    Either a class is loaded and all defined methods are available or it is not loaded and then you have nothing. What do you mean by _some of the methods (or fields) may not be loaded in the classpath_ ? – Axel Amthor May 12 '15 at 18:18
  • I offer a helper method that does the work to retrieve a registered service, but if the plugin isn't there it just isn't used; but reflection throws an error because of this. – Nahydrin May 12 '15 at 18:21
  • 1
    AFAIK [`Class#getMethods`](http://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getMethods--) does not throw `ClassNotFoundException` –  May 12 '15 at 18:22
  • please show the constructor of `Reflection` – Axel Amthor May 12 '15 at 18:26
  • @AxelAmthor I modified the post, it includes the constructor and toPrimitiveTypeArray. – Nahydrin May 12 '15 at 18:30
  • What I'm missing is, where you actually load the class like `this.class = class.forname(this.packageName + ":" + this.className ..etc ....);` That's probably why you get a `NoClassDefFoundError ` ...? – Axel Amthor May 12 '15 at 18:36
  • @RC. I moved the `Class#getMethods()` out of the loop, and it errors on that test line now. – Nahydrin May 12 '15 at 18:37
  • @AxelAmthor The class isn't what's throwing the error, I already pointed out where the error is. I added the methods you wanted to see anyway. – Nahydrin May 12 '15 at 18:38
  • 1
    Assuming the class doesn't exist how could the method do? I mean what do you expect the `Method` object to be (a method belong to a class). –  May 12 '15 at 18:39
  • @RC. null, empty, not there, etc. I'm not even trying to find that method, it errors when I try to access something else. – Nahydrin May 12 '15 at 18:40
  • Simple: If there is no class, then there are no methods. – Axel Amthor May 12 '15 at 18:40
  • if null is OK how is a try catch not good? –  May 12 '15 at 18:42
  • If the class isn't loaded, it's obvious that any attempt to acces their methods must fail with arbitrary erorros. I can not see where any classloader is invoked to load the class stored in `className`, `PackagePath` and `SubPackage`. – Axel Amthor May 12 '15 at 18:43

2 Answers2

2

If you keep your design, you are creating non-portable code that relies on non-guaranteed behavior.

If a method’s return type refers to a non-available class, it doesn’t help if the method’s code attempts to handle the absence of the class (return null or whatever) or that the method is not called.

It’s not only the Class.getMethods which may fail, it may cause the entire class to be unusable. The problem is that the JVM has some freedom regarding when to throw an error if a directly referenced class cannot be resolved. One valid point is the class loading time, so with a different JVM, implementing such a resolving strategy, you wouldn’t even get so far that you can call Class.getMethods().

The only way to have valid code dealing with optional classes is to decouple the directly referenced classes from the optional classes, e.g.

public static BaseType getOptionalFeature() {
  try {
    return (BaseType)Class.forName("OptionalType").newInstance();
  } catch (ClassNotFoundException | InstantiationException | IllegalAccessException ex) {
    return null;
  }
}

Assuming that OptionalType extends or implements BaseType, the important aspect is that Baseype is a type always available and that all non-optional code always refers to the optional feature through BaseType. You may have more code accessing OptionalType directly, which you assume not to be called if OptionalType is not available, but it also must be detached from the non-optional code using dynamic class loading.

Otherwise you are risking a complete failure on eagerly resolving JVMs and spurious failures on lazily resolving JVMs like Oracle’s. The fact that it doesn’t immediately fail if you don’t call the problematic method is not a guaranteed behavior for yours sake— it’s just an optimization.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • The fact that it doesn’t immediately fail if you don’t call the problematic method is not a guaranteed behavior. Are you sure? If the method is not called, will JVM still fail? For example, if (false) { new Foo(); }. Foo is not in runtime classpath. Any doc link on JVM behavior for this? – eastwater Aug 10 '20 at 04:30
  • 1
    @Sunnyday when you write `if(false) { new Foo(); }`, the invocation will get removed at compile-time already, so the compiled class has no dependency to `Foo` (unless it references it somewhere else). That’s immune against JVM implementation details. Other uses without such compile-time code elimination are not immune. [This Q&A](https://stackoverflow.com/q/41965457/2711488) discusses a practical example how an absent unused class can make the program fail, depending on subtle code aspects, even with the commonly used reference implementation (OpenJDK/HotSpot JVM). – Holger Aug 10 '20 at 08:52
  • 1
    For an official reference, see [JLS §12.3](https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.3): “*This specification allows an implementation flexibility as to when linking activities (and, because of recursion, loading) take place, ... For example, an implementation may choose to resolve each symbolic reference in a class or interface individually, only when it is used (lazy or late resolution), or to resolve them all at once while the class is being verified (static resolution).*” – Holger Aug 10 '20 at 08:59
0

What about:

public Class<?> getClazz() {
    try {
        return Class.forName(this.getClassPath());
    } catch (ClassNotFoundException e) {
        // ignore or better log
    }
    return null;
}

then later:

Class<?> clazz = this.getClazz();
if (clazz != null) {
    for (Method method : clazz.getMethods()) {
        // ....
    }
}