Skip to content
Luke Hutchison edited this page Oct 11, 2026 · 28 revisions

The classgraph-classpath library works out where a running JVM loads its classes and resources from: the classpath elements, the module path, and the locations that a container's custom classloaders load from. It is the classpath finder that ClassGraph itself scans with, and it can be used on its own, without scanning anything. Replace X.Y.Z with the latest release number:

<dependency>
    <groupId>io.github.classgraph</groupId>
    <artifactId>classgraph-classpath</artifactId>
    <version>X.Y.Z</version>
</dependency>

If you already depend on classgraph, you have this library too: classgraph depends on classgraph-classpath, which depends on classgraph-vfs, which depends on classgraph-base.

Contents

Finding the classpath

import io.github.classgraph.classpath.*;

try (Classpath classpath = new ClasspathFinder().enableClasspath().enableNonSystemModules().find()) {
    for (ClasspathEntry entry : classpath) {
        System.out.println(entry.getLocation() + " (from " + entry.getClassLoaderName() + ")");
    }
    for (java.lang.module.ModuleReference module : classpath.getNonSystemModules()) {
        System.out.println(module.descriptor().name());
    }
}

find() opens the jarfiles on the classpath, in order to read their manifests, so Classpath is AutoCloseable: closing it closes those jarfiles again, and deletes any temporary files that were needed to open a jarfile nested inside another jarfile. The classpath elements and the modules can still be read after close(), so if you only want the list of locations, you can close the Classpath immediately:

List<String> locations;
try (Classpath classpath = new ClasspathFinder().enableClasspath().enableNonSystemModules().find()) {
    locations = classpath.getLocations();
}

Saying what to search

Nothing is searched unless it is enabled, so a ClasspathFinder with no enable method called returns an empty Classpath. The methods that say where to search come in pairs. The method with no arguments enables the sources that are found in the current runtime environment; the method that takes varargs enables exactly the sources it is given:

No arguments: search what is in the environment Varargs: search exactly what is named
enableClasspath() -- every classpath element of every classloader that can be found -- the thread context classloader, the system classloader, the classloader of the class in any frame of the call stack, and the ancestors of all of those, including java.class.path enableClassLoaders(ClassLoader...), enableClasspathEntries(Object...)
enableDetectedModuleLayers() -- the module layers that are visible to the caller, and the boot layer enableModuleLayers(ModuleLayer...)

Calling only the varargs method searches only what it names, and nothing from the environment. Calling both searches the environment as well as what you named. A method that searches the environment has no overload that searches only what is named, and a method that searches what is named has no overload that searches the environment.

enableSystemModules() and enableNonSystemModules() say which kinds of module to search for, not where to look for them. They search the module layers that are visible to the caller if you name no layers of your own; name a layer with enableModuleLayers(...), and only the layers you named are searched, unless you call enableDetectedModuleLayers() too.

Each call adds to the end of the list of things to search, so classpath sources are reported in the order the calls were made. Modules always come first, whatever order they were enabled in, since that is the order in which the JVM resolves a class.

What is searched

enableClasspath() finds every classpath element of every classloader that can be found in the environment. A classloader is found if it is any of the following:

  • the context classloader of the calling thread, as returned by Thread.currentThread().getContextClassLoader();
  • the classloader that loaded ClassGraph itself;
  • the system classloader, as returned by ClassLoader.getSystemClassLoader() -- this is the application classloader, unless the JVM was launched with -Djava.system.class.loader;
  • the classloader of the class in any frame of the current call stack, so that the classloader of the code that called ClassGraph is searched even when it is none of the above; or
  • an ancestor of any of those, reached through ClassLoader#getParent().

Every classpath element that every one of those classloaders loads classes from is found, whether or not the classloader exposes it publicly -- see Classpath specification mechanisms for how each supported classloader is read. Elements are reported in the order in which the classloaders that declared them would be asked to load a class. The application classloader is normally one of the classloaders found, so the entries that the java.class.path system property lists are searched too, at the position the application classloader takes in that order.

The classpath elements that a classpath element declares are found as well, and added at the position they are reached from: the jarfiles in its automatic lib dirs, and the entries of its manifest's Class-Path and Bundle-ClassPath attributes. Each of those may declare more, so this is recursive. A jarfile, a jarfile nested inside another jarfile, and an exploded jarfile in a directory all declare their child classpath elements the same way, so a manifest is honored wherever it is found. A child is resolved against the classpath element that declared it, so a relative Class-Path entry of a jarfile that lives in a non-default FileSystem names a sibling within that same filesystem. Class-Path entries are separated by any whitespace, and follow the JVM's rules for URLs: a jarfile in a filesystem can name another element by a file: URL, but by no other URL; a nested jarfile, or one fetched from a URL, can name no URL; and a jarfile fetched from a URL can only name paths below its own directory, by a relative URL with no .. in it. Allowing a URL scheme does not change this.

A classpath element that is reached more than once is listed only at the first position it is reached at, which is the position that decides which copy of a duplicated class is loaded. Whether two elements are the same one is decided by their location, and a location that names a file or directory is canonical, so the same jarfile reached through a symbolic link and reached directly is one element rather than two.

A classloader, or a jarfile's manifest, may name a file or directory that is not there, or that cannot be read. Such an element is logged and left out of the classpath rather than listed, since it can contribute no class. An element that is there but cannot be opened -- a file with a .jar name that is not a zipfile, say -- is still listed, because whether it can be opened is only found out when it is opened.

ClasspathFinder

An instance is not thread-safe while it is being configured, but find() may be called any number of times, and returns a new Classpath each time. The configuration methods return this, so they can be chained.

Method Effect
Classpath find() Find the classpath elements and the modules. Throws IllegalStateException if the thread was interrupted while the jarfiles were being read.
ClasspathFinder verbose() Log what is found to the io.github.classgraph.ClassGraph logger, at INFO level. For diagnosis only; not a stable output format.
ClasspathFinder enableRealtimeLogging() Write each log entry as soon as it is made, rather than only once the classpath has been found, to see where find() gets stuck. This says when the log is written, not whether there is one, so find() throws IllegalArgumentException unless verbose() is also called.
ClasspathFinder allowURLScheme(String scheme) Allow classpath elements to be named by a URL with the given scheme, e.g. "http". Every scheme the JVM has a handler for is allowed already, except http, https, ftp and mailto, which are denied to begin with because a classpath is not always something the caller wrote -- see custom URL schemes. An element whose scheme is denied is still reported, but is not opened, so the elements it declares are not found. Allowing a scheme also keeps a ':'-separated classpath string from being split at that scheme's own colon. A colon in the authority of a URL of any scheme -- before a port, in user:password@, or in an IPv6 address -- is never taken as a separator. This setting does not apply to the entries of a jarfile's Class-Path attribute -- see What is searched.
ClasspathFinder denyURLScheme(String scheme) Deny a further URL scheme, so that a classpath element named by a URL with that scheme, or given as a Path whose URI has that scheme, is reported but not opened.
ClasspathFinder disableNestedJarScanning() Do not open jarfiles nested within other jarfiles. The jarfiles in the lib dirs of a jarfile (e.g. BOOT-INF/lib/ in a Spring Boot jar, or WEB-INF/lib/ in a war file) are then left out of the result, while the jarfiles in the lib dirs of a directory are still found.
ClasspathFinder disableMultiReleaseVersions() Do not honor the multi-release versioning of a jarfile: the Vfs returned by Classpath#getVfs() reports every version of a multi-release resource under the META-INF/versions/<N>/ path it is stored under, rather than serving only the version the running JVM would use. This does not change which classpath elements find() returns.
ClasspathFinder setMaxBufferedJarRAMSize(int maxBufferedJarRAMSize) Set the most bytes of one jarfile that are held in RAM before the rest is spilled to a temporary file (default 64MB). This applies only to jarfiles that cannot be read in place: a nested jarfile stored deflated within its enclosing jarfile, a jarfile downloaded from a URL, and a jarfile read from an InputStream through Classpath#getVfs(). Throws IllegalArgumentException if the size is negative. This does not change which classpath elements find() returns.

Choosing what is searched

Method Effect
enableClasspath() Search every classpath element of every classloader that can be found in the environment -- the thread context classloader, ClassGraph's own classloader, the system classloader, the classloader of the class in any frame of the call stack, and the ancestors of all of those -- including the java.class.path entries of the application classloader. See What is searched.
enableClasspathEntries(String classpath) Search this classpath, with elements separated by File.pathSeparatorChar. No classloader is asked for it, so nothing else is searched unless it is enabled as well. This is the only overload that splits a classpath string. Throws IllegalArgumentException if the string holds no classpath element, e.g. if it is empty.
enableClasspathEntries(Object... elements) The same, but each argument is one classpath entry and is not split on File.pathSeparatorChar. A String, File, Path, URL or URI is kept in that form, and is what the entry is later opened with; any other Iterable, or an array of objects, is replaced by its own elements, to any depth, except that an Iterable that is not a Collection and yields only itself, as an Ant Resource does, is one entry, read by its toString(); an element of any other type is read by its toString(). So enableClasspathEntries(classpath, extraJar) takes a whole classpath string as one entry: to search a classpath string together with other entries, chain the calls, e.g. enableClasspathEntries(System.getProperty("java.class.path")).enableClasspathEntries(extraJar). Throws IllegalArgumentException if no entry is given, or if any element, at any depth, is a ClassLoader -- pass those to enableClassLoaders() instead -- or is a Stream (or any other BaseStream), an Iterator or an Enumeration that is not an Iterable -- collect it into a collection first, e.g. with stream.toList().
enableClasspathEntries(Iterable<?> elements) The same, for a collection. A single Path passed here is treated as one classpath entry, not as a sequence of its name elements.
enableClassLoaders(ClassLoader... classLoaders) Search these classloaders, and their parents, rather than the ones found in the environment. Call enableClasspath() as well to search both.
ignoreParentClassLoaders() Do not search the parents of the classloaders that are searched. Elements that only a parent declares are left out. This says only which classloaders to search, so it has no effect, and find() throws IllegalArgumentException, unless enableClasspath() or enableClassLoaders(...) is also called.
registerClassLoaderHandler(ClassLoaderHandler handler) Teach this library how to read the classpath out of a classloader that it does not already know about. Registered handlers are offered each classloader before the built-in handlers are, so a registered handler can also override a built-in one. When more than one handler can handle the same classloader, only the handlers that name the most specific classloader class are used, except that a handler you registered is never dropped this way. See writing a ClassLoaderHandler for an example. A handler is only offered the classloaders that are searched, so it has no effect, and find() throws IllegalArgumentException, unless enableClasspath() or enableClassLoaders(...) is also called. If a method of a handler throws a RuntimeException or a LinkageError, the exception is logged, and the classpath is found without that handler's help for that classloader: the classpath elements the handler added before it failed are kept.
filterClasspathEntries(Predicate<? super ClasspathEntry> filter) Keep only the classpath elements the filter accepts. It is given every classpath element found: those the classloaders or the classpath entries name, the jarfiles a wildcarded directory expands into, and those another element declares through its Class-Path or Bundle-ClassPath attribute or its lib dirs. A rejected element is not opened, so the elements it declares are found only if an accepted element declares them too. The filter may be called more than once for an element that is reached by more than one route. If this method is called more than once, an element is kept only if every filter accepts it. An exception thrown by the filter is thrown out of find(). This has no effect, and find() throws IllegalArgumentException, unless enableClasspath(), enableClassLoaders(...) or enableClasspathEntries(...) is also called.
enableSystemModules() Search the JDK's own modules in the module layers visible from the caller: the layers of the classes on the call stack, and the boot layer. A system module is a module whose name starts with java., jdk., javafx. or oracle., and which ModuleFinder.ofSystem() finds at the same location. The application's own modules are non-system modules, even when jlink or jpackage has linked them into the runtime image.
enableNonSystemModules() Search the non-system modules of those same layers.
enableModuleLayers(ModuleLayer... layers) Search these module layers, and their parent layers, rather than the layers visible from the caller. Use this if the calling code does not itself run within the module layer whose modules are wanted. The named layers replace the visible ones, so enableSystemModules() and enableNonSystemModules() then apply to the named layers. This says only where to look for modules, so it has no effect, and find() throws IllegalArgumentException, unless enableSystemModules() or enableNonSystemModules() is also called.
enableDetectedModuleLayers() Search the module layers visible from the caller as well as any named by enableModuleLayers(...). Only needed alongside enableModuleLayers(...): the visible layers are searched anyway when a kind of module is enabled and no layer is named. Like enableModuleLayers(...), it has no effect, and find() throws, unless enableSystemModules() or enableNonSystemModules() is also called.
ignoreParentModuleLayers() Do not search a module layer that is the parent of another module layer that is searched, so that only the modules of the innermost layers are found. Like enableModuleLayers(...), it has no effect, and find() throws, unless enableSystemModules() or enableNonSystemModules() is also called.

No configuration method calls another configuration method: every method changes exactly the one setting it names, so each option you rely on has to be called for by name. An option that is only read by another option -- the three module layer methods above, which say where to look for modules but not which kind to look for, ignoreParentClassLoaders() and registerClassLoaderHandler(...), which need a classloader to be searched, filterClasspathEntries(...), which needs a classpath source, and enableRealtimeLogging(), which needs a log -- is refused by find() with an IllegalArgumentException naming both methods, rather than being silently ignored.

Classpath

Method Returns
List<ClasspathEntry> getEntries() The classpath elements, in the order the classloaders would search them, including the elements declared by jarfiles on the classpath. Unmodifiable.
List<String> getLocations() The getLocation() of each entry, in the same order. Unmodifiable.
List<ModuleReference> getModules() The modules that were found: getSystemModules() followed by getNonSystemModules(). A jar on the traditional classpath is not a module and is not listed here, even when ClassGraph reports a module name for it. Empty unless enableSystemModules() or enableNonSystemModules() was called.
List<ModuleReference> getSystemModules() The JDK's own modules, in module layer order and by module name within each layer: the modules whose name starts with java., jdk., javafx. or oracle., and which ModuleFinder.ofSystem() finds at the same location. Empty unless enableSystemModules() was called.
List<ModuleReference> getNonSystemModules() The other modules, in the same order. Empty unless enableNonSystemModules() was called.
ModulePathInfo getModulePathInfo() The module path switches this JVM was launched with.
Vfs getVfs() The virtual filesystem that the jarfiles' manifests were read through, so that a classpath element can be read without opening it a second time. It has the settings the ClasspathFinder had when find() was called: changing the ClasspathFinder afterward does not change them. Pass it to ClasspathEntry#open(Vfs) to read a classpath element, or to Vfs#open(ModuleReference) to read a module. Closed by close(), so do not close it yourself, and do not let anything it hands out escape the block.
void close() Close the jarfiles that were opened to read their manifests, and delete any temporary files. Calling it more than once has no further effect.

A Classpath is Iterable<ClasspathEntry>, so a for-each loop over it iterates the same entries as getEntries(), in the same order. The modules are a separate list, and iterating a Classpath does not visit them.

toString() returns the classpath elements and then the modules, one per line.

Modules are reported as JDK ModuleReference objects: module.descriptor().name() is the module name, and module.location() is an Optional<URI> naming where it was loaded from.

ClasspathEntry

One element of the classpath: a directory or a jarfile that classes and resources are loaded from.

Method Returns
String getLocation() Where the element is. An absolute path for a local directory or jarfile, with / as the separator on every platform. A jarfile nested inside another jarfile is written in the Java outer.jar!/inner.jar form. Anything that is not a local file, e.g. an element served over HTTP by a container, is the URL or URI it was found as. A path is canonical, spelled the way the file is stored: symbolic links are resolved, and on Windows and macOS, so is the case of each name. Two elements are the same element if their locations are equal, so a file that two classloaders reach by different paths is listed once, at the first position it is reached at.
String getClassLoaderName() The toString() of the classloader this element was obtained from, or null if it did not come from a classloader: an element named with enableClasspathEntries(), or one that the manifest or a lib dir of such an element declared. Only the string is kept, so that finding the classpath does not keep a classloader alive.
List<String> getPackageRootPrefixes() The directory prefixes to look for within this element and strip if present, because a classloader of this type goes looking for the root of the package hierarchy below them, e.g. "WEB-INF/classes/" for a classloader that loads a webapp straight from its WAR file. These are the layouts the classloader could have used, not the ones this element actually uses, so a prefix is listed whether or not a directory with that name is there. Empty for a classloader whose classpath elements always have their classes at the root, which is every classloader that this library has a built-in handler for; never contains the empty string.
List<String> getLibDirPrefixes() The lib dirs whose jarfiles are added to the classpath if they are present within this element, because a classloader of this type loads from them without listing them as classpath elements, e.g. "lib/" and "main/" for the classloader of a One-JAR executable jarfile. As with getPackageRootPrefixes(), these are the lib dirs the classloader could load from, not the ones this element actually has. Empty for a classloader that lists every jarfile it loads from; never contains the empty string.
VfsRoot open(Vfs vfs) Open the element for reading, and return a VfsRoot over its contents. Pass classpath.getVfs(), so that a jarfile that was already opened to read its manifest is not read a second time. Throws IOException if the element could not be opened or read.

toString() is the location, followed by the classloader name in square brackets if there is one.

Reading a classpath element

try (Classpath classpath = new ClasspathFinder().enableClasspath().enableNonSystemModules().find()) {
    Vfs vfs = classpath.getVfs();
    for (ClasspathEntry entry : classpath) {
        for (VfsEntry resource : entry.open(vfs)) {
            System.out.println(resource.getPath());
        }
    }
}

open(Vfs) opens the element in whichever form the classloader named it in, rather than flattening it to getLocation() and parsing that back into an object. That matters for the forms a location does not reliably round-trip: a Path in a filesystem other than the default one is read through that filesystem, whether or not its toUri() form resolves back to it and whatever URL schemes are denied, and a URL keeps the scheme it was found with. A network scheme still has to be allowed with new ClasspathFinder().enableClasspath().allowURLScheme("https") before find(), or open throws IOException.

The forms a classpath element can be found in

ClasspathEntry is a sealed class, with one subclass per form a classloader can name an element in. Code that needs the object the element was found as can ask for it; code that only wants to read the element does not need to know which subclass it has.

Subclass Accessor Found when
ClasspathEntry.OfPathString -- The element was named by a string -- a path, or a URL written as a string -- which is the usual case, or by an object of a type this library does not recognize, whose toString() was taken as the location.
ClasspathEntry.OfFile File getFile() The element was named by a File. Spelled with getLocation(), so on Windows File#getPath() comes back with backslashes.
ClasspathEntry.OfPath Path getPath() The element was named by a Path. It may belong to a filesystem other than the default one, e.g. a jarfile mounted with FileSystems#newFileSystem, in which case getLocation() only names it: a path in a zip filesystem is named in the outer.zip!/dir form, and a path in any other filesystem by a URI string, and neither is necessarily equal to its toUri() form. The element is reached by opening the Path itself, through open(Vfs), while that filesystem is open.
ClasspathEntry.OfURL URL getURL() The element was named by a URL object.
ClasspathEntry.OfURI URI getURI() The element was named by a URI object.

Because the type is sealed, those five are the only possibilities, so a switch over them is checked for exhaustiveness:

String form = switch (entry) {
    case ClasspathEntry.OfPathString s -> "string";
    case ClasspathEntry.OfFile f -> "File " + f.getFile();
    case ClasspathEntry.OfPath p -> "Path in " + p.getPath().getFileSystem();
    case ClasspathEntry.OfURL u -> "URL served over " + u.getURL().getProtocol();
    case ClasspathEntry.OfURI u -> "URI " + u.getURI();
};

A switch with type patterns needs Java 21 or newer. On Java 17, test for each subclass with instanceof instead:

String form;
if (entry instanceof ClasspathEntry.OfFile f) {
    form = "File " + f.getFile();
} else if (entry instanceof ClasspathEntry.OfPath p) {
    form = "Path in " + p.getPath().getFileSystem();
} else if (entry instanceof ClasspathEntry.OfURL u) {
    form = "URL served over " + u.getURL().getProtocol();
} else if (entry instanceof ClasspathEntry.OfURI u) {
    form = "URI " + u.getURI();
} else {
    form = "string";
}

Two entries are equal if they were found in the same form, at the same location, from the same classloader, with the same package root prefixes and lib dir prefixes -- so the same jarfile named by a File in one classpath and by a Path in another gives two entries that are not equal. Within a single classpath the question does not arise: an element is listed once, at the first position it is reached at, whichever form it was named in there, since duplicates are recognized by location.

ModulePathInfo

The module system switches the JVM was launched with, read from the runtime's own commandline arguments. This covers only what was named on the commandline -- it does not include the traditional classpath, or the system modules, which the runtime adds by itself.

Each getter returns an unmodifiable, insertion-ordered Set of the switch's values, in the order they were given on the commandline. A switch that was not used gives an empty set. A switch that was used more than once contributes all of its values to the same set, except --module-path, for which only the last occurrence is reported, since that is the only one the JVM uses.

The commandline cannot change while the JVM runs, so there is one instance for the whole JVM, returned by ModulePathInfo.current(). Classpath#getModulePathInfo(), ClassGraph#getModulePathInfo() and ScanResult#getModulePathInfo() return the same object.

Method Switch One entry looks like
Set<String> getModulePath() --module-path / -p one module path element (the switch value is split on File.pathSeparatorChar)
Set<String> getAddModules() --add-modules a module name (the switch value is split on ,). ALL-DEFAULT, ALL-SYSTEM and ALL-MODULE-PATH are valid names here; see JEP 261
Set<String> getPatchModules() --patch-module <module>=<file>
Set<String> getAddExports() --add-exports <source-module>/<package>=<target-module>(,<target-module>)*
Set<String> getAddOpens() --add-opens <source-module>/<package>=<target-module>(,<target-module>)*
Set<String> getAddReads() --add-reads <source-module>=<target-module>

The values are read from the commandline only, so the manifest attributes of jarfiles found later do not change them.

Clone this wiki locally