| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Works out what is actually on the classpath and module path of a running JVM, and in what order. It does not read any of it -- there is no scanner and no classfile parser here. If you want to find classes, use classgraph instead, which is built on this library.
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph-classpath</artifactId>
<version>X.Y.Z</version>
</dependency>Module name: io.github.classgraph.classpath. Requires JDK 17 or newer. Depends on classgraph-vfs, which is pulled in transitively.
See the Classpath API for the full reference, and Classpath Specification Mechanisms for the list of places a classpath can come from.
That property holds the classpath the JVM was started with. It is empty or misleading in most non-trivial deployments, because the code that is actually running was loaded by a classloader that built its own classpath afterwards. This library asks the classloaders themselves:
try (Classpath classpath = new ClasspathFinder().find()) {
classpath.getLocations().forEach(System.out::println);
}getLocations() gives the location strings in classpath order. Closing the Classpath releases the file handles and temporary files taken while expanding Class-Path: manifest entries and nested jarfiles.
try (Classpath classpath = new ClasspathFinder().find()) {
for (ClasspathEntry entry : classpath) {
System.out.println(entry.getLocation()
+ " [classloader: " + entry.getClassLoaderName() + "]"
+ (entry.getPackageRootPrefixes().isEmpty() ? ""
: " [package roots: " + entry.getPackageRootPrefixes() + "]"));
}
}Iterating the Classpath iterates its entries, in classpath order; getEntries() returns the same list. getLocation() is an absolute path, or a nested path of the form outer.jar!/inner.jar, or a URL for anything that is not a local file -- so do not assume Path.of(entry.getLocation()) will succeed. A path is given in canonical form, spelled the way the file is stored: symbolic links are resolved, and on a filesystem that ignores case, so is the case of each name. That is also what decides whether two entries are the same, so a file that two classloaders reach by different paths is listed once, at the first position it is reached at. getPackageRootPrefixes() lists the prefixes to strip from entry names within that element, e.g. BOOT-INF/classes for a Spring Boot jarfile; it is empty for an ordinary jarfile.
An entry that names a file or directory the filesystem says is not there, or that cannot be read, is left out of the classpath rather than listed and then failed on, since it can contribute no class.
Combining this library with classgraph-vfs:
try (Classpath classpath = new ClasspathFinder().find()) {
// The virtual filesystem that the classpath was read through
Vfs vfs = classpath.getVfs();
for (ClasspathEntry entry : classpath) {
// A classpath entry can be a directory, a jarfile or a jarfile nested in another jarfile.
// The virtual filesystem opens all of them, and lists their contents the same way.
for (VfsEntry resource : entry.open(vfs)) {
if (resource.getName().endsWith(".class")) {
System.out.println(resource.getPath());
}
}
}
}classpath.getVfs() is the same Vfs that the jarfiles' manifests were read through while the classpath was being found, so the jarfiles are still open and their central directories have already been parsed -- opening a new Vfs() here instead would read every one of them a second time. It is closed by classpath.close(), along with every root and entry it handed out, so do not let those escape the try block.
entry.open(vfs) opens the classpath element in whichever form the classloader named it in -- a path string, a File, a Path, a URL or a URI -- rather than flattening it to getLocation() and parsing that back. 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. ClasspathEntry is a sealed type with one subclass per form, so code that needs the original object can ask for it:
if (entry instanceof ClasspathEntry.OfURL urlEntry) {
System.out.println("Served over " + urlEntry.getURL().getProtocol());
}A classpath entry can also be a URL for something that is not a local file. Any scheme the JVM has a handler for is opened, including one an application registered itself -- see custom URL schemes. The exceptions are the four schemes that fetch over a network: http, https, ftp and mailto are denied to begin with, because a classpath is not always something the caller wrote. An entry with a denied scheme is still reported, but open throws IOException for it, so the elements it declares are not found. Call new ClasspathFinder().enableURLScheme("https") before find() to allow one, which allows it both while the classpath is being found and on the Vfs that Classpath.getVfs() hands back; disableURLScheme(String) denies a further scheme.
Enabling a scheme is also what keeps a ':'-separated classpath string from being split at that scheme's own colon, so a custom scheme is worth naming for that reason even though it needs no enabling to be fetched from.
try (Classpath classpath = new ClasspathFinder().find()) {
for (ModuleReference module : classpath.getNonSystemModules()) {
System.out.println(module.descriptor().name());
}
}Modules are a separate list from the classpath entries: getModules() returns all of them, getSystemModules() the ones from the JDK itself, and getNonSystemModules() the rest. Iterating the Classpath alone will not see them. A module's contents are read through the same virtual filesystem as a classpath element:
try (Classpath classpath = new ClasspathFinder().find()) {
Vfs vfs = classpath.getVfs();
for (ModuleReference module : classpath.getNonSystemModules()) {
for (VfsEntry resource : vfs.open(module)) {
System.out.println(resource.getPath());
}
}
}try (Classpath classpath = new ClasspathFinder().find()) {
ModulePathInfo modulePathInfo = classpath.getModulePathInfo();
System.out.println("--module-path: " + modulePathInfo.getModulePath());
System.out.println("--add-modules: " + modulePathInfo.getAddModules());
System.out.println("--patch-module: " + modulePathInfo.getPatchModules());
System.out.println("--add-exports: " + modulePathInfo.getAddExports());
System.out.println("--add-opens: " + modulePathInfo.getAddOpens());
System.out.println("--add-reads: " + modulePathInfo.getAddReads());
}try (Classpath classpath = new ClasspathFinder()
.overrideClasspath(Path.of("/path/to/a.jar"), Path.of("/path/to/classes"))
.find()) {
classpath.getLocations().forEach(System.out::println);
}The varargs and Iterable overloads take one classpath entry per element. A String, File, Path, URL or URI is kept in that form, and is what entry.open(vfs) later opens; anything else is read by its toString(). The String overload instead takes a whole path and splits it on the platform's path separator. Related switches: overrideClassLoaders(...) and addClassLoader(...) to control which classloaders are consulted, ignoreParentClassLoaders() to stop at the given one, overrideModuleLayers(...) and addModuleLayer(...) for JPMS layers, and disableModuleScanning() to skip the module path entirely.
Check first whether you need to: a URLClassLoader subclass is read automatically, and only needs a handler of its own if it searches its classpath in a different order, as Spring Boot's restart classloader does.
Otherwise, implement ClassLoaderHandler and register it. This is the extension point that lets ClassGraph support a container it has never seen:
public class MyClassLoaderHandler implements ClassLoaderHandler {
@Override
public boolean canHandle(Class<?> classLoaderClass, ClassGraphLog log) {
return classIsOrExtendsOrImplements(classLoaderClass, "com.xyz.MyClassLoader");
}
@Override
public void findClassLoaderOrder(ClassLoader classLoader, ClassLoaderOrder classLoaderOrder,
ClassGraphLog log) {
// Search this classloader before its parent (i.e. this one is parent-last)
classLoaderOrder.add(classLoader, log);
classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log);
}
@Override
public void findClasspathOrder(ClassLoader classLoader, ClasspathOrder classpathOrder,
ClassGraphLog log) {
for (URL url : ((MyClassLoader) classLoader).getRepositoryURLs()) {
classpathOrder.addClasspathEntry(url, classLoader, log);
}
}
}The log handed to each method is null unless verbose logging is switched on, which it is not by default. Pass it straight through to the methods that take one, as above -- every one of them accepts null -- and null-check it before calling it yourself.
try (Classpath classpath = new ClasspathFinder()
.registerClassLoaderHandler(new MyClassLoaderHandler())
.find()) {
classpath.getLocations().forEach(System.out::println);
}addClasspathEntry accepts a String, File, Path, URL or URI, and returns false if the entry was rejected -- because it is empty, because the filesystem says it is not there or that it cannot be read, because a classpath element filter rejected it, or because it was already added. There is no need to check any of that before calling: hand it everything the classloader offers, in the order the classloader would search it. A handler must be stateless, since one instance handles every classloader in every scan, and scans can run concurrently; state belonging to a single scan goes in the ClasspathOrder that is passed in. The same handler can be registered with the scanner via new ClassGraph().registerClassLoaderHandler(...).
Two more methods say what to look for within each classpath element the handler contributes, and both default to the layouts that any classloader can be handed: getPackageRootPrefixes() to classes/, test-classes/, BOOT-INF/classes/ and WEB-INF/classes/, and getLibDirPrefixes() to BOOT-INF/lib/, WEB-INF/lib/ and WEB-INF/lib-provided/. Override either one to add a dir that is specific to your container, keeping the defaults with ClassLoaderHandler.prefixesPlus(...):
@Override
public List<String> getLibDirPrefixes() {
return ClassLoaderHandler.prefixesPlus(ARCHIVE_LIB_DIR_PREFIXES, "my-container-lib/");
}Add a prefix only if the classloader really can produce classpath elements in that layout. BOOT-INF and WEB-INF are unambiguous, because a hyphen is not legal in a Java identifier, so a directory with one of those names cannot be a package; an ordinary name like classes/ or lib/ can be, and declaring one wrongly either hides a real package or puts jarfiles that are only resources on the classpath.
If a handler is useful to more than your own project, please open a pull request so it can ship with ClassGraph, registered alongside the built-in handlers in ClassLoaderHandlerRegistry.
try (Classpath classpath = new ClasspathFinder().verbose().find()) {
classpath.getLocations().forEach(System.out::println);
}The log is written to the io.github.classgraph.ClassGraph logger at INFO level when the Classpath is closed. It is a debugging aid, not a stable output format.
Since JDK 16 the module system has enforced strong encapsulation, and a classloader that only exposes its classpath through a private field cannot be read by reflection. If a classloader's classpath comes back empty and the same code worked on JDK 15 or earlier, that is the reason. The fixes are to add io.github.toolfactory:narcissus to the classpath (an optional dependency of classgraph-base that reads fields natively), or to open the relevant module with --add-opens.
MIT. See LICENSE-ClassGraph.txt.
| Back | FazBrowse Home | New Git URL |