2

I'm having this weird issue where a class from some transitive dependency keeps showing up at runtime, shadowing a newer version of the class from the (correct) first level dependency, even though I thought I made sure that I excluded the older version from all other dependencies I declare (this is in a Maven/IntelliJ setup)

More specifically, at runtime the app fails with a NoClassDefFoundError, since during class loading a wrong version of the owning class is loaded, which has a field of a type that does not exist in newer versions of the library that class is defined in. To illustrate:

// lib.jar:wrong-version
class Owner {
  private SomeType f;
}

// lib.jar:new-version
class Owner {
  private OtherType f;
}

At runtime, the class loader finds a reference to the symbol Owner and attempts to load the version that has SomeType, which in return does not exist anymore. This is even though I excluded wrong-version where ever I could spot it.

I also ran mvn dependency:tree to see if the old version is still being pulled in somewhere, but it's not!

In order to further debug this, I was wondering if there is a way to find out where a class loader was reading a specific class from, i.e. which file? Is that possible? Or even better, build a list of origins where a certain symbol is defined, in case it's defined more than once?

Sorry if this is vague, but the problem is rather nebulous.

mxk
  • 43,056
  • 28
  • 105
  • 132

2 Answers2

1

If you know the fully qualified name of the class, say somelib.Owner, you can try calling the following in your code:

public void foo() {
    URL url = somelib.Owner.class.getClassLoader().getResource("somelib/Owner.class");
    System.out.println(url);
}
M A
  • 71,713
  • 13
  • 134
  • 174
  • 1
    I'd make this `somelib.Owner.class.getClassLoader()...` just to make sure you're using the right classloader. – user253751 Aug 09 '14 at 08:51
  • Not sure I understand how the path would be constructed? If there's a Maven artifact (say com.company:artifact:v1), how would I construct the path to that class file? – mxk Aug 09 '14 at 15:34
  • It is just a '/'-separated path corresponding to the full classname, e.g. if the fully qualified name of the owning class is `foo.bar.Owner` the path would be `foo/bar/Owner.class`. Does the `NoClassDefFoundError` show the name of the owning class? – M A Aug 09 '14 at 19:09
  • @manouti it shows the name of the owned class, in my sample that would be `SomeType`. That's because the owner type does exist in both versions of the JAR, but has different implementations where in the newer version a certain field type has been removed – mxk Aug 11 '14 at 07:38
1

The following code will search the whole classpath for a particular class. With no arguments it will dump every class it finds and then you can pipe to grep or redirect to a file. It looks inside jars...

Usage: WhichClass or WhichClass package.name (note no .class)

Apologies for the lack of comments ...

import java.io.File;
import java.io.IOException;

import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public final class WhichClass {

  private WhichClass() {
  }

  static Vector<String> scratchVector;

  public static void main(final String[] argv) {
    Vector v;

    if ((argv.length == 0) || "-all".equals(argv[0])) {
      v = findClass(null);
    } else {
      v = findClass(argv[0]);
    }

    for (int i = 0; i < v.size(); i++) {
      System.out.println(v.elementAt(i));
    }
  }

  static String className(final String classFile) {
    return classFile.replace('/', '.').substring(0, classFile.length() - ".class".length());
  }

  static Vector findClass(final String className) {
    if (className != null) {
      scratchVector = new Vector<String>(5);
    } else {
      scratchVector = new Vector<String>(5000);
    }

    findClassInPath(className, setupBootClassPath());
    findClassInPath(className, setupClassPath());

    return scratchVector;
  }

  static void findClassInPath(final String className, final StringTokenizer path) {
    while (path.hasMoreTokens()) {
      String pathElement = path.nextToken();

      File pathFile = new File(pathElement);

      if (pathFile.isDirectory()) {
        try {
          if (className != null) {
            String pathName = className.replace('.', System.getProperty("file.separator").charAt(0)) + ".class";

            findClassInPathElement(pathName, pathElement, pathFile);
          } else {
            findClassInPathElement(className, pathElement, pathFile);
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      } else if (pathFile.exists()) {
        try {
          if (className != null) {
            String pathName = className.replace('.', '/') + ".class";

            ZipFile  zipFile  = new ZipFile(pathFile);
            ZipEntry zipEntry = zipFile.getEntry(pathName);
            if (zipEntry != null) {
              scratchVector.addElement(pathFile + "(" + zipEntry + ")");
            }
          } else {
            ZipFile     zipFile = new ZipFile(pathFile);
            Enumeration entries = zipFile.entries();

            while (entries.hasMoreElements()) {
              String entry = entries.nextElement().toString();

              if (entry.endsWith(".class")) {
                String name = className(entry);

                scratchVector.addElement(pathFile + "(" + entry + ")");
              }
            }
          }
        } catch (IOException e) {
          System.err.println(e + " while working on " + pathFile);
        }
      }
    }
  }

  static void findClassInPathElement(final String pathName, final String pathElement, final File pathFile)
    throws IOException {
    String[] list = pathFile.list();

    for (int i = 0; i < list.length; i++) {
      File file = new File(pathFile, list[i]);

      if (file.isDirectory()) {
        findClassInPathElement(pathName, pathElement, file);
      } else if (file.exists() && (file.length() != 0) && list[i].endsWith(".class")) {
        String classFile = file.toString().substring(pathElement.length() + 1);

        String name = className(classFile);

        if (pathName != null) {
          if (classFile.equals(pathName)) {
            scratchVector.addElement(file.toString());
          }
        } else {
          scratchVector.addElement(file.toString());
        }
      }
    }
  }

  static StringTokenizer setupBootClassPath() {
    String classPath = System.getProperty("sun.boot.class.path");
    String separator = System.getProperty("path.separator");

    return new StringTokenizer(classPath, separator);
  }

  static StringTokenizer setupClassPath() {
    String classPath = System.getProperty("java.class.path");
    String separator = System.getProperty("path.separator");

    return new StringTokenizer(classPath, separator);
  }
}
DavidPostill
  • 7,734
  • 9
  • 41
  • 60