0

I have a very simple program that attempts to read class files under a given package. This works fine locally (JDK 11) but when I run it on a Red Hat Linux 8 server (JDK 11), the same exact code returns back an empty array [].

Here is the code:

public static Set<Class> getClassesFromPackage(String packageName) throws ClassNotFoundException {
    logger.debug(String.format("Getting classes from package %s", packageName));
    InputStream stream = ClassLoader.getSystemClassLoader().getResourceAsStream(packageName.replaceAll("[.]", "/"));
    logger.debug("stream: " + stream);
    if (stream != null) {
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
        return reader.lines()
                     .filter(line -> line.endsWith(".class"))
                     .map(line -> getClass(line, packageName))
                     .filter(clazz -> clazz != null)
                     .collect(Collectors.toSet());
    } else {
        throw new ClassNotFoundException(String.format("package name %s doesn't exist", packageName));
    }
}

I'm calling it as follows:

Set<Class> classes = getClassesFromPackage("foo");
logger.info("classes: " + classes);

Locally I get the following:

classes: [class foo.Test]

On the server, I get the following:

classes: []

There are no errors thrown or logged.

Any ideas?

Abra
  • 19,142
  • 7
  • 29
  • 41
alessandro ferrucci
  • 1,261
  • 2
  • 24
  • 48

1 Answers1

0
ClassLoader.getSystemClassLoader().getResourceAsStream(packageName.replaceAll("[.]", "/"));

This doesn't work. If it works on windows, that's.. broken coincidence. The spec clearly says this doesn't work.

What you want, is impossible. ClassLoaders, fundamentally, do not support the idea of 'please give me every class in package X'. They just don't. The one and only primitive they have is 'give me resource X'. That's all you get. Given a class name such as java.lang.String, you can derive its fully qualified binary name from that and slash it up appropriately (java/lang/String.class), and then ask for that resource, which is a thing the ClassLoader API supports.

But, you can't take a package and ask for a listing. Simply because ClassLoader has no listResources method. You are trying to 'read' the resource java/lang and expecting this to return a listing of files. The API spec does not indicate that a ClassLoader has to work this way, and in fact, most don't, as you have now discovered.

SPI is a solution for allowing 'listing' of things, but SPI is an opt-in system, whether you use java.util.ServiceLoader with META-INF/services/com.foo.someInterfaceName style, or using the newer SPI stuff in module-info.java - the 'provider' has to opt into being listed.

Which gets us back to: Nope. You cannot do what you are trying to do.

But I need to

Well, and I want a pony. And world peace. Your only real option is to hack it. Which means: Every new version of java may break your code, and it's possible (likely even) that some combination of JVM version, JVM provider (azul, openjdk, adoptium, etc), and OS breaks your stuff and there is nobody who will accept your bug reports on it, because it's not a bug.

If you're willing to sign up to that gigantic headache, you can hack it. Specifically, you can ask for a well known resource such as .getResource("java/lang/String.class"), toString() the URL object you get, and then go to town on it: Figure out what that URL means and have code for each and every kind of resource URL you expect that knows how to unpack that URL and do the work.

The URL returned by ClassLoaders can be literally anything, up to and including data: URLs or customized blob://whatever style URLs. It's therefore literally impossible: You can't write static code that can respond to a world where anybody can write a custom classloader. However, the vast majority of classloaders out there have file, jar or jmod URLs. So, if you write a handler for all 3, you can handle most, but not all, distribution strategies.

file: is easy enough - convert to a Path, lop off the String.class, lang, and java parts, and then add the package you are interested in, and now you have a Path object that you can toss at Files.newDirectoryStream and voila - you have your class listing. This is not a listing of all classes in the package, that's not possible. This is merely a list of all classes in the package from that particular source - there could be more (you can have 2 entries on the classpath that both have the same package. This is common, even, where one place hosts the actual code and another place hosts unit tests in the same package).

For jar: URLs, find the !, turn the stuff in between jar: and ! into a path, open it as a new JarFile, and look for entries with the right package prefix and you again have what you wanted.

For jmod: URLs thats a bit more tricky - but the jmod tool exists and can list jmod contents, and you may have to peruse this SO question about how to read jmod files.

Yes, that's a ton of effort. Yes, that's the point - you're working around a fundamental problem which is that the classloader API simply does not allow you to list package contents and nevertheless you insist on doing it anyway. That usually results in 'lots of fragile, hard to maintain code'.

Another option is to find a library that does this hacky fragile stuff for you. Perhaps the reflections library can do this.

NB: It's clear you got this line from this baeldung tutorial - usually baeldung is high quality. This entry.. isn't.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • 2
    Excellent explanation. The only line that I disagree with is your last line. I have never found Baeldung’s pages to be high quality. Usually the pages have a handful of examples that address very specific cases that are difficult or impossible to adapt to any other use case, with no links to documentation that would cover the general case. – VGR Apr 04 '23 at 12:49
  • 1
    @VGR and there’s a lot of half-truths and outright wrong information too. – Holger Apr 05 '23 at 08:27
  • 2
    I don’t know any place in the specification that explicitly says that getting a resource for a directory is impossible. However, it only works if the underlying storage has directory entries. Using the `FileSystem` API, you can get [a solution is general as possible](https://stackoverflow.com/a/36021165/2711488). It works for `file:`, `jar:`, and `jrt:` URLs. And in case of modules, it does list all resources of a package. There is no need to support `jmod:` URLs, as JMOD files are not used by a class loader at all; they are used by `jlink` to create a new execution environment. – Holger Apr 05 '23 at 08:37
  • @Holger not so much 'impossible' as: "Not, at all, guaranteed", and that's enough to put the nail in the coffin. `jmod:` is indeed wrong; `String.class.getResource("/java/lang/String.class")` returns `jrt:/java.base/java/lang/String.class`. – rzwitserloot Apr 05 '23 at 12:28
  • And for what its worth, `getResource("jrt:/java.base/java/lang/")` doesn't work (returns `null`). – rzwitserloot Apr 05 '23 at 12:34
  • 1
    Correct. Trying `getResource` for directories works for the default file system (`file:` URLs) and might work for jar files (`jar:` URL) if the file has a pseudo entry for the directory. There’s no requirement to have such entries, so it depends on the deployment tool or used options whether such entries are generated. And that’s just for the URL; whether you can read it as `InputStream` is another question. For the module runtime image of the JDK, it will never work. That’s why the safe approach is to get the location of an existing (file-like) resource and use the filesystem API to navigate. – Holger Apr 05 '23 at 14:52
  • 1
    Side note, `getResource("jrt:/java.base/java/lang/")` is wrong, the correct idiom would be `ClassLoader.getSystemResource("java/lang")` or `Object.class.getResource("/java/lang")`, but it doesn’t work anyway. On the other hand, `Paths.get(URI.create("jrt:/java.base/java/lang/"))` does work (JDK 13+), so you can do `try(Stream s = Files.list(Paths.get( URI.create("jrt:/java.base/java/lang/")))) { s.forEach(System.out::println); }` when you know beforehand that your code will run on a modular JDK. – Holger Apr 05 '23 at 15:13