Skip to content
Luke Hutchison edited this page Aug 15, 2026 · 5 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.

<dependency>
    <groupId>io.github.classgraph</groupId>
    <artifactId>classgraph-classpath</artifactId>
    <version>LATEST</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().find()) {
    for (ClasspathEntry entry : classpath) {
        System.out.println(entry.location() + " (from " + entry.classLoaderName() + ")");
    }
    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().find()) {
    locations = classpath.getLocations();
}

What is searched, by default

By default, the classloaders found in the environment are searched: the context classloader of the calling thread, the classloader of the caller's own class, the system classloader, and their parents, in the order in which they would be asked to load a class. The java.class.path system property and the module system are consulted too.

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 honoured 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.

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. A classpath element that could not be opened, or that is not a jarfile, is still reported -- a classloader may name a jarfile or directory that is not there.

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 enableURLScheme(String scheme) Allow classpath elements to be named by a URL with the given scheme, e.g. "http". Only file: URLs are allowed by default. An element with a scheme that has not been enabled is still reported, but is not opened, so the elements it declares are not found.

Narrowing or widening what is searched

Method Effect
overrideClasspath(String classpath) Search this classpath instead of the one the environment declares, with elements separated by File.pathSeparatorChar. The classloaders, java.class.path and the modules are all ignored.
overrideClasspath(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; an element of any other type is read by its toString().
overrideClasspath(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.
overrideClassLoaders(ClassLoader... classLoaders) Search these classloaders instead of the ones found in the environment. This also causes java.class.path to be ignored.
addClassLoader(ClassLoader classLoader) Search this classloader as well as the ones found in the environment. Can only widen the search, never narrow it, and has no effect if the classpath is overridden.
ignoreParentClassLoaders() Do not search the parents of the classloaders that are searched. Elements that only a parent declares are left out.
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. See writing a ClassLoaderHandler for an example.
overrideModuleLayers(ModuleLayer... layers) Search these module layers instead of the ones visible from the caller. Use this if the calling code does not itself run within the module layer whose modules are wanted.
addModuleLayer(ModuleLayer layer) Search this module layer as well as the ones visible from the caller.
ignoreModules() Do not look for modules at all, so Classpath#getModules() is empty and only classpath elements are reported.

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 location() of each entry, in the same order. Unmodifiable.
List<ModuleReference> getModules() The modules this JVM can see, system modules first, each group in name order. Includes the automatic modules created for jars on the classpath. Empty if ignoreModules() was called, or if the classpath was overridden.
List<ModuleReference> getSystemModules() The modules whose name starts with java., jdk., javafx. or oracle., in name order -- the ones that ship with the JDK.
List<ModuleReference> getNonSystemModules() The modules other than those, in name order.
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. 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 location() 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. The location is not checked, so it may name something that does not exist.
String classLoaderName() The toString() of the classloader this element was obtained from, or null if it did not come from a classloader -- for example an entry from java.class.path, or from an overridden classpath. Only the string is kept, so that finding the classpath does not keep a classloader alive.
List<String> packageRootPrefixes() The directory prefixes to look for within this element and strip if present, because a classloader of this type can put the root of the package hierarchy below them, e.g. "BOOT-INF/classes/" for a Spring Boot jar. 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 elements always have their classes at the root; never contains the empty string.
List<String> libDirPrefixes() 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. "BOOT-INF/lib/" for a Spring Boot jar. As with packageRootPrefixes(), 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().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 location() and parsing that back into an object. That matters for the forms a location cannot round-trip: a Path in a filesystem other than the default one is reached only through its own filesystem, and a URL keeps the scheme it was found with. A remote scheme still has to be allowed with new ClasspathFinder().enableURLScheme("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 path 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 file() The element was named by a File. Spelled with location(), so on Windows File#getPath() comes back with backslashes.
ClasspathEntry.OfPath Path path() 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 location() names it but nothing can be opened by that name.
ClasspathEntry.OfURL URL url() The element was named by a URL, or by a URI or path string with a scheme that had to be parsed as a URL.
ClasspathEntry.OfURI URI uri() The element was named by a URI that did not have to be parsed as a URL.

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

if (entry instanceof ClasspathEntry.OfURL urlEntry) {
    System.out.println("Served over " + urlEntry.url().getProtocol());
}

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 -- 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, and a switch that was used more than once contributes all of its values to the same set.

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>

getAddExports() and getAddOpens() also include any Add-Exports and Add-Opens entries found in the manifests of jarfiles that were scanned, in the form <source-module>/<package>=ALL-UNNAMED. addExportsEntry(String) and addOpensEntry(String) are what the scanner calls to add those.

Clone this wiki locally