-
-
Notifications
You must be signed in to change notification settings - Fork 309
Porting ClassGraph 4.x code to ClassGraph 5.x
ClassGraph 5.x does not maintain source or binary compatibility with ClassGraph 4.x. This page is a complete, step-by-step guide to porting a project from the 4.x API to the 5.x API.
Most of the work is mechanical renaming, and the compiler finds nearly all of it for you. The part that needs attention is Step 8: a small number of changes leave your code compiling, but make it behave differently.
- Porting with Claude Code
- Step 1: Update the dependency
- Step 2: Update
module-info.java - Step 3: Rename the deprecated methods
- Step 4: Rename the hierarchy and annotation queries
- Step 5: Rename everything else
- Step 6: Replace the removed features
- Step 7: Fix the code that no longer compiles
- Step 8: Review the changes that compile but behave differently
- Step 9: Rebuild and test
- Optional: adopt the new capabilities
- Appendix A: internal package moves
- Appendix B: the ClassGraph 4.x documentation
This guide is written to be followed literally, by a person or by a coding agent. If you use Claude Code, open a terminal in your project's git repository and give it this prompt:
Port this repository from the ClassGraph 4.x API to the ClassGraph 5.x API, by following https://github.com/classgraph/classgraph/wiki/Porting-ClassGraph-4.x-code-to-ClassGraph-5.x exactly. Work through the steps in order. Do not skip Step 8 — those are the changes that still compile but behave differently, so the compiler will not find them for you. When you are done, build the project and run its tests, and tell me every place where you had to make a judgment call.
Commit your work first, so that you can read the diff afterwards and revert if you need to.
Two things to tell it if they apply to your project: that you are porting a library whose own API exposes ClassGraph types (so the renames propagate to your users), and any place where you rely on ClassGraph's behavior in a way the guide should not silently change.
ClassGraph 4.x shipped as one jar. ClassGraph 5.x is split into several, so that a project depends only on the part of ClassGraph it uses. Each artifact depends on the one above it:
| Artifact | Java module | Contents |
|---|---|---|
io.github.classgraph:classgraph-base |
io.github.classgraph.base |
Shared internals; no API of its own |
io.github.classgraph:classgraph-vfs |
io.github.classgraph.vfs |
A read-only virtual filesystem over directories, jarfiles and modules |
io.github.classgraph:classgraph-classpath |
io.github.classgraph.classpath |
Finding the classpath and the module path |
io.github.classgraph:classgraph |
io.github.classgraph |
Scanning, and the class graph API |
io.github.classgraph:classgraph-viz |
io.github.classgraph.viz |
GraphViz .dot file generation |
If you use the scanning API (new ClassGraph(), ScanResult, ClassInfo and the rest), keep
depending on classgraph — the artifact id and group id are unchanged, and the other artifacts come
in transitively:
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<version>5.0.0</version>
</dependency>If you also generate GraphViz .dot files, add classgraph-viz as well — the generators are no
longer in the core artifact (see Step 6).
If you use only the classpath finder or only the virtual filesystem, you can now depend on
classgraph-classpath or classgraph-vfs alone. See
the new capabilities.
ClassGraph 5.x requires JDK 17 or newer, to build and to run. If your project still targets JDK 8 through 16, stay on the last 4.x release; it is not going away.
If your project is modular, the requires io.github.classgraph; line still covers the scanning API,
and nothing needs to change. Add a requires line for each further ClassGraph module you use:
requires io.github.classgraph; // Scanning and the class graph API
requires io.github.classgraph.viz; // Only if you generate .dot files
requires io.github.classgraph.classpath; // Only if you use ClasspathFinder directly
requires io.github.classgraph.vfs; // Only if you use Vfs directlyIf you use maven-shade-plugin, the packages to relocate have changed: there is no longer a nonapi
top-level package, so relocating io.github.classgraph now covers all of ClassGraph. See
Shading ClassGraph.
Everything that was deprecated in 4.x has been removed. Each has a direct replacement with the same behavior, so these are pure renames:
| ClassGraph 4.x | ClassGraph 5.x |
|---|---|
ClassGraph#whitelistPackages |
ClassGraph#acceptPackages |
ClassGraph#whitelistPackagesNonRecursive |
ClassGraph#acceptPackagesNonRecursive |
ClassGraph#whitelistPaths |
ClassGraph#acceptPaths |
ClassGraph#whitelistPathsNonRecursive |
ClassGraph#acceptPathsNonRecursive |
ClassGraph#whitelistClasses |
ClassGraph#acceptClasses |
ClassGraph#whitelistJars |
ClassGraph#acceptJars |
ClassGraph#whitelistModules |
ClassGraph#acceptModules |
ClassGraph#whitelistClasspathElementsContainingResourcePath |
ClassGraph#acceptClasspathElementsContainingResourcePath |
ClassGraph#blacklistPackages |
ClassGraph#rejectPackages |
ClassGraph#blacklistPaths |
ClassGraph#rejectPaths |
ClassGraph#blacklistClasses |
ClassGraph#rejectClasses |
ClassGraph#blacklistJars |
ClassGraph#rejectJars |
ClassGraph#blacklistModules |
ClassGraph#rejectModules |
ClassGraph#blacklistClasspathElementsContainingResourcePath |
ClassGraph#rejectClasspathElementsContainingResourcePath |
ScanResult#getResourcesWithPathIgnoringWhitelist |
ScanResult#getResourcesWithPathIgnoringAccept |
FieldInfo#getModifierStr |
FieldInfo#getModifiersString |
In 4.x, getSubclasses(), getInterfaces(), getAnnotationInfo() and the rest returned the
transitive answer, and you narrowed it to the directly-related classes by calling .directOnly().
Nothing in the name said which one you were getting. Each query is now spelled out as either
getAll... (transitive — what 4.x returned) or getDirect... (what .directOnly() gave you).
Read this table carefully. The 4.x name is gone, so your code will not compile until you choose one of the two columns, and the safe default — the one that preserves 4.x behavior — is the middle column, not the right one.
| ClassGraph 4.x | Same behavior as 4.x | Direct only |
|---|---|---|
ClassInfo#getSubclasses() |
ClassInfo#getAllSubclasses() |
ClassInfo#getDirectSubclasses() |
ClassInfo#getSuperclasses() |
ClassInfo#getAllSuperclasses() |
ClassInfo#getSuperclass() (single) |
ClassInfo#getInterfaces() |
ClassInfo#getAllSuperinterfaces() |
ClassInfo#getDirectSuperinterfaces() |
ClassInfo#getClassesImplementing() |
ClassInfo#getAllClassesImplementing() |
ClassInfo#getDirectClassesImplementing() |
ClassInfo#getSubinterfaces() |
ClassInfo#getAllSubinterfaces() |
ClassInfo#getDirectSubinterfaces() |
ClassInfo#getAnnotations() |
ClassInfo#getAllAnnotations() |
ClassInfo#getDirectAnnotations() |
ClassInfo#getAnnotationInfo() |
ClassInfo#getAllAnnotationInfo() |
ClassInfo#getDirectAnnotationInfo() |
ClassInfo#getAnnotationInfo(Class | String) |
ClassInfo#getAllAnnotationInfo(Class | String) |
ClassInfo#getDirectAnnotationInfo(Class | String) |
ClassInfo#getAnnotationInfoRepeatable(Class | String) |
ClassInfo#getAllAnnotationInfoRepeatable(...) |
ClassInfo#getDirectAnnotationInfoRepeatable(...) |
MethodInfo / FieldInfo #getAnnotationInfo(...)
|
#getAllAnnotationInfo(...) |
#getDirectAnnotationInfo(...) |
MethodInfo / FieldInfo #getAnnotationInfoRepeatable(...)
|
#getAllAnnotationInfoRepeatable(...) |
#getDirectAnnotationInfoRepeatable(...) |
MethodParameterInfo#getAnnotationInfo(...) |
#getAllAnnotationInfo(...) |
#getDirectAnnotationInfo(...) |
MethodParameterInfo#getAnnotationInfoRepeatable(...) |
#getAllAnnotationInfoRepeatable(...) |
#getDirectAnnotationInfoRepeatable(...) |
PackageInfo / ModuleInfo #getAnnotationInfo(...)
|
(see the warning below) | #getDirectAnnotationInfo(...) |
ScanResult#getSubclasses(Class | String) |
ScanResult#getAllSubclasses(...) |
ScanResult#getDirectSubclasses(...) |
ScanResult#getSuperclasses(Class | String) |
ScanResult#getAllSuperclasses(...) |
getSuperclass() on the ClassInfo
|
ScanResult#getInterfaces(Class | String) |
ScanResult#getAllSuperinterfaces(...) |
ScanResult#getDirectSuperinterfaces(...) |
ScanResult#getClassesImplementing(Class | String) |
ScanResult#getAllClassesImplementing(...) |
ScanResult#getDirectClassesImplementing(...) |
ScanResult#getSubinterfaces(Class | String) |
ScanResult#getAllSubinterfaces(...) |
ScanResult#getDirectSubinterfaces(...) |
ScanResult#getAnnotationsOnClass(String) |
ScanResult#getAllAnnotationsOnClass(...) |
ScanResult#getDirectAnnotationsOnClass(...) |
Five notes:
-
PackageInfoandModuleInfoare the exception. Their 4.xgetAnnotationInfo(...)returned only the annotations written onpackage-info.class/module-info.class, without expanding meta-annotations. So for these two classes, the 4.x behavior isgetDirectAnnotationInfo(...), notgetAllAnnotationInfo(...). Both classes now carry the same twelve-method annotation surface asClassInfo, sogetAllAnnotationInfo()on a package or module now expands meta-annotations, andhasAnnotation()(which asks the "all" list) answers true for a meta-annotation. -
.directOnly()still works, onClassInfoListandAnnotationInfoList. If your code already saysgetSubclasses().directOnly(),getAllSubclasses().directOnly()is a correct port; butgetDirectSubclasses()is the direct way to ask. -
The reverse queries deliberately keep their names:
ScanResult#getClassesWithAnnotation(...),#getClassesWithAllAnnotations(...),#getClassesWithAnyAnnotation(...),#getClassesWithMethodAnnotation(...),#getClassesWithFieldAnnotation(...)and the rest. They return the transitive answer, as in 4.x. -
ClassInfoList#getInterfaces()andClassInfoList#getAnnotations()are unchanged. They are list filters ("keep the entries of this list that are interfaces"), not hierarchy queries. Do not rename them. - The no-argument
ScanResult#getAllInterfaces()— "every interface in the scan result" — is unchanged. Only the two per-class overloads are renamed.
All twelve annotation methods now come from a new public interface, HasAnnotations, implemented by
ClassInfo, FieldInfo, MethodInfo, MethodParameterInfo, PackageInfo and ModuleInfo. This is
additive — every method keeps its name and signature — but it lets you write a method that accepts any
annotated element:
static boolean isDeprecated(HasAnnotations element) {
return element.hasAnnotation(Deprecated.class);
}| ClassGraph 4.x | ClassGraph 5.x |
|---|---|
ClassInfo#getModifiersStr() |
ClassInfo#getModifiersString() |
ClassInfo#getTypeSignatureStr() |
ClassInfo#getTypeSignatureString() |
ClassMemberInfo#getModifiersStr() |
ClassMemberInfo#getModifiersString() |
ClassMemberInfo#getTypeDescriptorStr() |
ClassMemberInfo#getTypeDescriptorString() |
ClassMemberInfo#getTypeSignatureStr() |
ClassMemberInfo#getTypeSignatureString() |
ClassMemberInfo#getTypeSignatureOrTypeDescriptorStr() |
ClassMemberInfo#getTypeSignatureOrTypeDescriptorString() |
FieldInfo#getModifiersStr() |
FieldInfo#getModifiersString() |
MethodInfo#getModifiersStr() |
MethodInfo#getModifiersString() |
MethodParameterInfo#getModifiersStr() |
MethodParameterInfo#getModifiersString() |
ArrayClassInfo#getTypeSignatureStr() |
ArrayClassInfo#getTypeSignatureString() |
ArrayTypeSignature#getTypeSignatureStr() |
ArrayTypeSignature#getTypeSignatureString() |
BaseTypeSignature#getTypeStr() |
BaseTypeSignature#getTypeName() |
BaseTypeSignature's method is the exception to the pattern: it returns the name of a primitive type
("int", "void"), not a signature string, so it is named after Class#getTypeName(), which returns
the same string for the same type.
| ClassGraph 4.x | ClassGraph 5.x |
|---|---|
Resource#getLastModified() |
Resource#getLastModifiedMillis() |
ScanResult#classpathContentsLastModifiedTime() |
ScanResult#getClasspathContentsLastModifiedMillis() |
ScanResult#classpathContentsModifiedSinceScan() |
ScanResult#isClasspathContentsModifiedSinceScan() |
ModuleInfo#getLocation() |
ModuleInfo#getLocationURI() |
Both time values were already in milliseconds since the epoch, and are unchanged; the names now say
so. Resource#getLastModifiedMillis() still returns 0L when the time is unknown.
| ClassGraph 4.x | ClassGraph 5.x |
|---|---|
getParameterValues() |
getParameterValues() |
getParameterValues(true) |
getParameterValues() |
getParameterValues(false) |
getDeclaredParameterValues() |
getDefaultParameterValues(), which returns the defaults declared by the annotation type, is
unchanged.
ModuleRef was a wrapper written when ClassGraph had to run on JDK 7 and 8, where
java.lang.module did not exist. It is gone, and the JDK's own type is used:
| ClassGraph 4.x | ClassGraph 5.x |
|---|---|
ClassInfo#getModuleRef() |
ClassInfo#getModuleReference() |
ModuleInfo#getModuleRef() |
ModuleInfo#getModuleReference() |
Resource#getModuleRef() |
Resource#getModuleReference() |
ScanResult#getModules() |
ScanResult#getModuleReferences() |
ClassGraph#getModules() |
ClassGraph#getModuleReferences() |
Then rewrite the accessor calls on the returned object:
ModuleRef (4.x) |
ModuleReference (5.x) |
|---|---|
getName() |
descriptor().name() |
getReference() |
the object itself |
getDescriptor() |
descriptor() |
getPackages() |
descriptor().packages() |
getRawVersion() |
descriptor().rawVersion().orElse(null) |
getLocation() |
location().orElse(null) |
getLocationString() |
location().map(URI::toString).orElse(null) |
open() |
open() |
ModuleRef#getPackages() returned the package names sorted; descriptor().packages() returns
them in no particular order. Sort them yourself if the order matters.
Four ModuleRef members have no equivalent:
-
getLayer()— you supplied the layer toaddModuleLayer(), or it isModuleLayer.boot(). ClassGraph no longer holds it, because holding it kept that layer's classloaders alive. -
getClassLoader()— loading classes is now your job; see Step 6. -
isSystemModule()— this was a scanning-time classification, not a property of a module, and is now internal to ClassGraph. -
getLocationFile()— uselocation(), which gives you theURI.
| ClassGraph 4.x | ClassGraph 5.x |
|---|---|
ClassGraph#addModuleLayer(Object) |
ClassGraph#addModuleLayer(ModuleLayer) |
ClassGraph#overrideModuleLayers(Object...) |
ClassGraph#overrideModuleLayers(ModuleLayer...) |
If you already passed a ModuleLayer, no source change is needed — but the call must be recompiled.
Open a module with ModuleReference#open(), which returns a java.lang.module.ModuleReader:
// ClassGraph 4.x
try (ModuleReaderProxy moduleReader = moduleRef.open()) {
List<String> paths = moduleReader.list();
ByteBuffer content = moduleReader.read(path);
}
// ClassGraph 5.x
try (ModuleReader moduleReader = moduleReference.open()) {
List<String> paths = moduleReader.list().toList();
ByteBuffer content = moduleReader.read(path).orElseThrow();
}Three differences to handle: list() returns a Stream<String>, not a List<String>; open(String),
read(String) and find(String) return Optional values and throw IOException rather than wrapping
it in an unchecked exception; and close() throws IOException, where ModuleReaderProxy#close()
swallowed it.
ModulePathInfo has moved from io.github.classgraph to io.github.classgraph.classpath — update
the import. It is still reachable from ScanResult#getModulePathInfo(), and now also from
Classpath#getModulePathInfo().
Its six public final Set<String> fields are now private, behind getters that return an unmodifiable
Set<String>:
| ClassGraph 4.x | ClassGraph 5.x |
|---|---|
modulePathInfo.modulePath |
modulePathInfo.getModulePath() |
modulePathInfo.addModules |
modulePathInfo.getAddModules() |
modulePathInfo.patchModules |
modulePathInfo.getPatchModules() |
modulePathInfo.addExports |
modulePathInfo.getAddExports() |
modulePathInfo.addOpens |
modulePathInfo.getAddOpens() |
modulePathInfo.addReads |
modulePathInfo.getAddReads() |
ClassGraph reads classfiles; it does not load them any more.
Class loading was the source of many of the hardest-to-understand bugs in 4.x. Loading a class means picking a classloader for it, and the right choice depends on details that cannot be reliably predicted from outside the application: which loader delegates to which, which one has the class visible to it, whether loading it will run static initializers with side effects, and what the framework the code is running under expects. 4.x tried to anticipate the right answer in every environment, and every environment it had not anticipated produced another bug. The application knows which classloader it wants; ClassGraph does not, so it no longer guesses.
Everything that turned a scan result into a live Class, Method, Field, Constructor, enum
constant or annotation instance has been removed, along with the ClassGraphClassLoader that did the
loading:
| Removed in 5.x |
|---|
ScanResult#loadClass(String, boolean) and #loadClass(String, Class<T>, boolean)
|
ClassInfo#loadClass(), #loadClass(boolean), #loadClass(Class<T>), #loadClass(Class<T>, boolean)
|
ClassInfo#getEnumConstantObjects() |
ClassInfoList#loadClasses(), #loadClasses(boolean), #loadClasses(Class<T>), #loadClasses(Class<T>, boolean)
|
ArrayClassInfo#loadClass(), #loadClass(boolean), #loadElementClass(), #loadElementClass(boolean)
|
ArrayTypeSignature#loadClass(), #loadClass(boolean), #loadElementClass(), #loadElementClass(boolean)
|
ClassRefTypeSignature#loadClass() and #loadClass(boolean)
|
AnnotationClassRef#loadClass() and #loadClass(boolean)
|
AnnotationEnumValue#loadClassAndReturnEnumValue() and #loadClassAndReturnEnumValue(boolean)
|
AnnotationInfo#loadClassAndInstantiate() |
FieldInfo#loadClassAndGetField() |
MethodInfo#loadClassAndGetMethod() and #loadClassAndGetConstructor()
|
ClassGraph#initializeLoadedClasses() |
ClassInfo#getClassLoader() |
The public class ClassGraphClassLoader
|
Load classes yourself, with a classloader you hold:
// ClassGraph 4.x
Class<?> cls = classInfo.loadClass();
// ClassGraph 5.x
Class<?> cls = Class.forName(classInfo.getName(), /* initialize = */ false, myClassLoader);The natural classloader to hold is the one you scanned with:
try (URLClassLoader classLoader = new URLClassLoader(urls);
ScanResult scanResult = new ClassGraph().overrideClassLoaders(classLoader)
.enableAllInfo().scan()) {
ClassInfo classInfo = scanResult.getClassInfo("com.xyz.Widget");
Class<?> cls = Class.forName(classInfo.getName(), false, classLoader);
}From the loaded Class, reflection gives you methods, fields, constructors, enum constants and
annotation instances directly, from the JDK, with the JDK's semantics.
Before you write any of that, check whether you needed to load the class at all. Most 4.x code
that called loadClass() did so only to reach information that the scan already has:
| If you loaded a class in order to... | Read it from the scan result instead |
|---|---|
| read an annotation's parameters |
AnnotationInfo#getParameterValues() (includes defaults) |
| read an annotation's parameters as written | AnnotationInfo#getDeclaredParameterValues() |
| read an enum constant in an annotation |
AnnotationEnumValue#getClassName() / #getValueName()
|
| read a class reference in an annotation | AnnotationClassRef#getName() |
| list an enum's constants | ClassInfo#getEnumConstants() |
| read method or field signatures and modifiers |
MethodInfo / FieldInfo
|
ClassInfo#getClassLoaderString() replaces ClassInfo#getClassLoader(). It returns the toString()
of the classloader the classfile was found under — an identifier for logging and for working out where
a class came from, not a classloader you can load with. It is null if the class was not itself scanned,
or was found in a module loaded by the bootstrap classloader.
More generally: no object a 5.x scan produces holds a classloader. Not ScanResult, not
ClassInfo, not Resource. The classloaders and module layers you pass to addClassLoader(),
overrideClassLoaders(), addModuleLayer() and overrideModuleLayers() are held by the ClassGraph
instance itself, so the same instance can still be scanned with more than once, but they are not
reachable from the ScanResult. If your code was relying on a ScanResult keeping a classloader
alive, hold your own reference to it.
ScanResult can no longer be serialized to JSON, or read back from it:
| Removed in 5.x |
|---|
ScanResult#toJSON() and #toJSON(int indentWidth)
|
ScanResult#fromJSON(String json) |
ScanResult#isObtainedFromDeserialization() |
There is no replacement, and pointing a JSON library at a ScanResult (or at ClassInfo,
MethodInfo, FieldInfo and the rest) is not supported either. If you need scan results as JSON,
read the values you need out of the ScanResult and generate your own JSON from them — which also
gives you a format you control, rather than one that changed whenever ClassGraph's internal fields
changed. A serialized 4.x ScanResult could only ever be read back by the same version of ClassGraph
that wrote it.
If you were using this to cache a scan across JVM runs, the replacement is to define a small record of just the facts you need, and serialize that.
.dot file generation has moved out of the core artifact, and is no longer reached through
ClassInfoList. Add the io.github.classgraph:classgraph-viz dependency, and call the static methods
of io.github.classgraph.viz.GraphVizDotFile, which take the ScanResult the classes came from,
followed by the classes to graph:
// ClassGraph 4.x
String dot = scanResult.getAllClasses().generateGraphVizDotFile();
// ClassGraph 5.x
String dot = GraphVizDotFile.generate(scanResult, scanResult.getAllClasses());The eight overloads — the widest of which took two floats and six booleans — are replaced by
GraphVizDotFileOptions, whose no-argument constructor holds the defaults and whose methods each
switch one option away from its default:
GraphVizDotFile.generate(scanResult, scanResult.getAllClasses(),
new GraphVizDotFileOptions().layoutSize(12, 8).hideFields().hideMethods());ClassGraph 4.x (on ClassInfoList) |
ClassGraph 5.x (on GraphVizDotFile) |
|---|---|
generateGraphVizDotFile() |
generate(scanResult, classes) |
generateGraphVizDotFile(float, float) |
generate(scanResult, classes, options) |
generateGraphVizDotFile(float, float, boolean × 5) |
generate(scanResult, classes, options) |
generateGraphVizDotFile(float, float, boolean × 6) |
generate(scanResult, classes, options) |
generateGraphVizDotFile(File) |
write(scanResult, classes, path) |
generateGraphVizDotFileFromInterClassDependencies() |
generateFromInterClassDependencies(scanResult, classes) |
generateGraphVizDotFileFromInterClassDependencies(float, float) |
generateFromInterClassDependencies(scanResult, classes, options) |
generateGraphVizDotFileFromInterClassDependencies(float, float, boolean) |
generateFromInterClassDependencies(scanResult, classes, options) |
generateGraphVizDotFileFromClassDependencies() (deprecated) |
generateFromInterClassDependencies(scanResult, classes) |
The options, with their defaults: layoutSize(10.5f, 8.0f); hideFields(),
hideFieldTypeDependencyEdges(), hideMethods(), hideMethodTypeDependencyEdges(),
hideAnnotations() and hideAnnotationDependencyEdges() (everything is shown by default, subject to
the corresponding ClassGraph#enable*Info() call having been made before the scan);
useFullyQualifiedNames() (simple names by default); and includeExternalClasses() /
excludeExternalClasses() (by default the inter-class dependency graph follows the scan's own
enableExternalClasses() setting).
Four things to check when porting a call:
-
showAnnotations = falsemaps tohideAnnotationDependencyEdges(), nothideAnnotations(). In 4.x that flag hid only the annotation edges, and there was no way to leave annotations out of the class boxes.hideAnnotations()is the new option that does the latter. -
writetakes ajava.nio.file.Path, not ajava.io.File, and returns thatPathrather than the list it was called on. It writes UTF-8; 4.x wrote the platform default charset, which mangled non-ASCII class and member names on any platform whose default was not UTF-8. -
An empty list of classes now produces an empty graph, where 4.x threw
IllegalStateException("List is empty"). Scanning withoutenableClassInfo(), or callinggenerateFromInterClassDependencieswithoutenableInterClassDependencies(), still throwsIllegalStateException. -
The rendering has changed slightly: annotation edges no longer include the shortcut edges that
4.x drew from a class to its annotations' meta-annotations and to its superclasses'
@Inheritedannotations (those annotations are still in the graph, still reachable along a path of edges); annotations within a class box are now sorted rather than in classfile order; and long annotation lists now wrap instead of making the node very wide. If you compare generated .dot files against stored expected output, regenerate that output.
writeFromInterClassDependencies(scanResult, classes, path) is new — in 4.x the dependency graph
could only be generated as a string.
ClassGraph's eleven single-method interfaces are gone, replaced by the JDK functional interfaces they duplicated:
| Removed interface | Now takes |
|---|---|
ClassInfoList.ClassInfoFilter |
Predicate<ClassInfo> |
AnnotationInfoList.AnnotationInfoFilter |
Predicate<AnnotationInfo> |
FieldInfoList.FieldInfoFilter |
Predicate<FieldInfo> |
MethodInfoList.MethodInfoFilter |
Predicate<MethodInfo> |
PackageInfoList.PackageInfoFilter |
Predicate<PackageInfo> |
ModuleInfoList.ModuleInfoFilter |
Predicate<ModuleInfo> |
ResourceList.ResourceFilter |
Predicate<Resource> |
ClassGraph.ClasspathElementFilter |
Predicate<String> |
ClassGraph.ClasspathElementURLFilter |
Predicate<URL> |
ClassGraph.ScanResultProcessor |
Consumer<ScanResult> |
ClassGraph.FailureHandler |
Consumer<Throwable> |
A lambda or method reference compiles unchanged — the shape of the argument is the same. What has to change is code that names one of the types (an anonymous class, a field holding a filter, a class that implements one), and the method name inside it:
- the filters'
accept(...)becomesPredicate#test(...) -
ScanResultProcessor#processScanResult(...)becomesConsumer#accept(...) -
FailureHandler#onFailure(...)becomesConsumer#accept(...)
In exchange you get the JDK's combinators: filter(p.negate()), filter(p1.and(p2)),
filter(p1.or(p2)).
ResourceList's ByteArrayConsumer, InputStreamConsumer and ByteBufferConsumer are kept,
because their methods throw IOException and no JDK functional interface does.
In 4.x you added Narcissus to your project and selected it in code. In 5.x, adding the dependency is the whole of it — ClassGraph looks for Narcissus when it starts up and uses it if it is present:
| Removed in 5.x | Use instead |
|---|---|
ClassGraph.CircumventEncapsulationMethod (enum) |
Nothing — just add the Narcissus dependency |
ClassGraph#getCircumventEncapsulationMethod() |
Nothing |
ClassGraph#setCircumventEncapsulationMethod(...) |
Nothing |
Delete those calls. If your project sets the 4.x field ClassGraph.CIRCUMVENT_ENCAPSULATION, delete
that too.
One thing to be ready for: on JDK 24+, loading Narcissus's native library draws a warning unless the
JVM was launched with --enable-native-access=ALL-UNNAMED (JEP 472).
Add that flag to your launch command. This is not cosmetic for long — the JEP states that restricted
methods will be refused outright in a later release.
Adding Narcissus can change what a scan finds, which is the reason to add it: without it, ClassGraph
cannot read the JDK classloaders' private ucp field on JDK 16+, and falls back to the
java.class.path system property, which does not cover classpath entries added by a Java agent, boot
classpath appends, the parents of a classloader passed to overrideClassLoaders(), or entries a
search path holds only in internal bookkeeping.
Removed; delete the call. ClassGraph now decides by platform: it memory-maps on Windows, and reads through the file channel on Linux and macOS. There is nothing to configure.
The decision came out of a benchmark across all three platforms, two JDKs, three workloads and both warm and cold page caches. Its conclusions:
- Windows: mapping is 16% to 38% faster, on both JDKs and all three workloads, with the ranges not even overlapping. This is the largest single effect measured.
- Linux: 0% to 10% faster warm, a wash cold — and up to 37% slower on a cold page cache when the classpath is mostly resources rather than classfiles, which is what a real Maven repository looks like. A page fault on a mapping fetches the kernel's whole fault-around window, so classfiles scattered among resources that ClassGraph never reads cost extra device traffic: measured device reads went from 31% of the corpus to 50% of it.
- macOS: inside the noise, in both directions.
- Mapping also handles errors worse: a jar truncated mid-scan gives a clean end-of-file through a
positioned read, but a
java.lang.InternalErrorfrom a signal handler through a mapping. - Two things that sound like reasons were tested and are not: mapping does not make a scanned jar any
harder to delete than reading it does (on Windows neither can be deleted while the
ScanResultis open, and on Linux and macOS both can), andFileChannelmonitor contention is not what mapping is avoiding on Linux — the gap is the same 4% at 1 thread and at 32.
Mapping everywhere would regress the cold, resource-heavy Linux case; mapping nowhere would give up the Windows win; and leaving it as an opt-in asks a question no one can answer without repeating the whole exercise on their own workload. The numbers, the method and the tools are on the Memory mapping benchmark page.
Removed; delete the calls. The JRE/JDK extension mechanism they searched was removed from the JDK in
JDK 9 (JEP 220), so on every JDK that ClassGraph 5.x supports there are no such jars to match.
Relatedly, ClassGraph#enableSystemJarsAndModules() now only affects system modules and packages; it
no longer adds any jars to the classpath.
Each of the three forEach* families had grown three method names and two consumer interfaces. Each
is now two methods — one that propagates IOException, one that ignores it:
| ClassGraph 4.x | ClassGraph 5.x |
|---|---|
forEachByteArrayThrowingIOException(ByteArrayConsumerThrowsIOException) |
forEachByteArray(ByteArrayConsumer) |
forEachByteArray(ByteArrayConsumer, boolean) |
forEachByteArray or forEachByteArrayIgnoringIOException
|
forEachByteArray(ByteArrayConsumer) |
forEachByteArray(ByteArrayConsumer), but see the warning below |
forEachByteArrayIgnoringIOException(ByteArrayConsumer) |
unchanged |
and likewise for forEachInputStream and forEachByteBuffer. The
ByteArrayConsumerThrowsIOException, InputStreamConsumerThrowsIOException and
ByteBufferConsumerThrowsIOException interfaces are deleted; ByteArrayConsumer,
InputStreamConsumer and ByteBufferConsumer now declare throws IOException on accept.
forEachByteArray(consumer) is a silent behavior change — see
Step 8.
These six methods now return the ResourceList, rather than void, so they can be chained. Code that
ignores the return value is unaffected.
By this point the renames are done, and the compiler will point at everything that is left. The remaining errors will be one of these:
| ClassGraph 4.x | ClassGraph 5.x |
|---|---|
MethodInfo#getParameterInfo() → MethodParameterInfo[]
|
→ List<MethodParameterInfo>
|
MethodInfo#getThrownExceptionNames() → String[]
|
→ List<String>
|
Replace array.length with list.size(), array[i] with list.get(i), and for (X x : array) needs
no change. MethodInfo#getThrownExceptions() already returned a ClassInfoList and is unchanged, and
Resource#load() still returns byte[] (it is file content, not a collection, and each call already
returns a fresh array).
There was one way to read a resource as a buffer and get it back closeable, and another way to read
it and have to remember to close the Resource yourself. There is now one way:
// ClassGraph 4.x
ByteBuffer byteBuffer = resource.read();
try {
// ... use byteBuffer ...
} finally {
resource.close();
}
// ClassGraph 4.x, the other way
try (CloseableByteBuffer buf = resource.readCloseable()) {
ByteBuffer byteBuffer = buf.getByteBuffer();
// ... use byteBuffer ...
}
// ClassGraph 5.x
try (CloseableByteBuffer buf = resource.read()) {
ByteBuffer byteBuffer = buf.getByteBuffer();
// ... use byteBuffer ...
}Delete the readCloseable in a readCloseable() call and it compiles unchanged. A read() call
needs .getByteBuffer() adding to reach the buffer, and its result belongs in a try-with-resources.
Closing the returned buffer closes the Resource it came from, so a separate resource.close() is
no longer needed, though it is still harmless. ResourceList#forEachByteBuffer() is unaffected --
it still hands the consumer a plain ByteBuffer, and still closes it afterwards.
Nothing in it can throw one. Drop the catch (IOException) around a CloseableByteBuffer used in a
try-with-resources: if there is nothing else in the block that can throw IOException, the catch
has to go, since catching a checked exception that cannot be thrown does not compile.
ScanResult, Resource and CloseableByteBuffer implement java.lang.AutoCloseable rather than
java.io.Closeable. None of their close() methods throws IOException, which is the only thing
Closeable adds. try-with-resources is unaffected, and so is any call to close(). Only code that
names the interface breaks: assigning one of these to a Closeable variable, or passing it to a
method that takes a Closeable. Use AutoCloseable instead.
It extends RuntimeException directly — a failed scan is not a bad argument, and catching
IllegalArgumentException around scan() also caught unrelated argument errors. Nothing fails to
compile, since both are unchecked, but a catch (IllegalArgumentException e) around scan() no
longer catches it. Catch ClassGraphException by name.
Several members were public or protected even though their types are internal, so nothing outside
ClassGraph could usefully call or override them. They are now package-private. If your code touches
any of these, it was reaching into ClassGraph's internals and needs a different approach:
ScanResult#reflectionUtils- the
public Resource(ClasspathElement, long)constructor - the
protected findReferencedClassInfo(Map, Set, LogNode)methods ofClassInfo,MethodInfo,MethodInfoList,FieldInfo,FieldInfoList,AnnotationInfo,AnnotationInfoList,AnnotationParameterValue,AnnotationParameterValueList,TypeSignature,ClassTypeSignatureandMethodTypeSignature - the
protected addTypeAnnotationmethods ofHierarchicalTypeSignature,TypeSignatureand their subclasses TypeArgument#findReferencedClassNames(Set)- the
protectedfields ofClassInfo(name,typeSignatureStr,isExternalClass,isScannedClass,classfileResource),ClassMemberInfo(declaringClassName,name,modifiers,typeDescriptorStr,typeSignatureStr,annotationInfo),Resource(inputStream,byteBuffer,length) andHierarchicalTypeSignature(typeAnnotationInfo) — every one of these already had a public getter, so use that - the
protectedconstructors ofClassInfo,ClassMemberInfo,HierarchicalTypeSignature,TypeSignature,ReferenceTypeSignature,ClassRefOrTypeVariableSignatureandTypeParameter, andMethodParameterInfo#setScanResult— these classes only ever come from a scan, so subclassing them outside ClassGraph never worked
The API is annotated @NullMarked: every type in a signature is non-null
unless it is explicitly annotated @Nullable. Nothing changes at runtime — this documents behavior
that was already the case — but if you use a null-analysis tool that understands JSpecify (IntelliJ
IDEA, Eclipse JDT, NullAway, Kotlin), it will now flag calls that dereference a nullable ClassGraph
result without checking it, and calls that pass null where ClassGraph does not accept null. Those
are pre-existing latent bugs in your code, not new restrictions — fix them rather than suppressing
them.
Kotlin callers see ClassGraph types as proper platform-free types: a @Nullable return is T?, and
everything else is T rather than T!. Kotlin code that was written against T! may now fail to
compile where it was implicitly treating a @Nullable return as non-null; that is the same latent
bug, made visible.
You do not need to add a JSpecify dependency: ClassGraph declares it in provided scope, so it is not
a transitive dependency of your project, and the JVM ignores annotations whose type is absent. Add
org.jspecify:jspecify to your own build only if you want your tools to read the annotations.
This is the step the compiler cannot do for you. Each item below names what to search for.
4.x hid java.lang.Object from the class graph. 5.x does not, which changes four things:
-
ClassInfo#getSuperclass()now returnsObjectfor a standard class that extends no other class, instead of null. As withClass#getSuperclass(), null is now returned only forObjectitself and for interfaces. -
ClassInfo#getAllSuperclasses()andScanResult#getAllSuperclasses(...)now end withObject, if the whole superclass chain was scanned. In 4.xObjectwas always excluded. - If
java.lang.Objectis accepted, it is now scanned like any other class, so it appears ingetAllClasses()and its members can be read. It also appears ingetAllClasses()andgetAllStandardClasses()wheneverenableExternalClasses()is called. -
getAllSubclasses("java.lang.Object")still returns every standard class in the scan result, as before.
Search for: getSuperclass, and any loop that walks up a superclass chain. A loop that runs until
getSuperclass() returns null now takes one more step, and a null check that read as "this class
extends nothing" now needs to compare the name against "java.lang.Object":
// ClassGraph 4.x
for (ClassInfo ci = classInfo; ci != null; ci = ci.getSuperclass()) { ... }
// ClassGraph 5.x — if you do not want to visit Object
for (ClassInfo ci = classInfo; ci != null && !ci.getName().equals("java.lang.Object");
ci = ci.getSuperclass()) { ... }Rendered output is unchanged: ClassInfo#toString() and the type signature classes still leave out an
extends java.lang.Object clause, and neither the .dot file nor getClassDependencies() includes
Object.
4.x had three different glob dialects, and which one you got depended on which method you called.
There is now one, shared by every accept/reject criterion and by
ScanResult#getResourcesMatchingWildcard():
| Wildcard | Matches |
|---|---|
* |
zero or more characters within one package or path segment |
** |
zero or more whole segments — must form a complete segment on its own |
? |
exactly one character, other than the separator |
Every other character is matched literally, including regex metacharacters.
Package and path globs do not change. acceptPackages, rejectPackages, acceptPaths and
rejectPaths have used exactly these rules since ClassGraph 4.8.187, so if you are coming from
4.8.187 or later, their arguments port unchanged. (If you are coming from 4.8.185 or earlier, * used
to cross . and / there too, so read the table below for those as well.)
What does change is every other glob. Search for: each string literal passed to
acceptClasses, rejectClasses, acceptModules, rejectModules, acceptJars, rejectJars,
acceptClasspathElementsContainingResourcePath,
rejectClasspathElementsContainingResourcePath and getResourcesMatchingWildcard. A glob with no
wildcard in it, or with a * that was only ever meant to match within one segment, needs no change.
Otherwise:
| What you wrote in 4.x | What to write in 5.x | Why |
|---|---|---|
acceptClasses("*.*Suffix") |
acceptClasses("**.*Suffix") |
* no longer crosses .
|
acceptModules("java.*") |
acceptModules("java.**") |
same — "java.*" now matches java.base but not java.xml.crypto
|
acceptClasspathElementsContainingResourcePath("META-INF/*") |
...("META-INF/**") |
same, for /
|
getResourcesMatchingWildcard("**.txt") |
getResourcesMatchingWildcard("**/*.txt") |
** must be a whole segment; the old form now throws IllegalArgumentException
|
getResourcesMatchingWildcard("[abc].txt") |
getResourcesMatchingPattern(Pattern) |
regex syntax is no longer passed through to the regex engine |
a jar or resource glob containing ?
|
getResourcesMatchingPattern(Pattern), or escape it |
? was matched literally by the class/jar/module globs, and now matches one character |
Two 4.x behaviors change in the direction of matching more, so a glob that used to match nothing may now start matching. Both are bug fixes, and both are also fixed in the final 4.x release:
- A class, jar or module glob containing a regex metacharacter other than
.had that character copied into the pattern unescaped, so it was interpreted as regex syntax — the$in the binary name of a nested class acted as an end-of-input anchor, andacceptClasses("com.Outer$Inner*")matched nothing. -
getResourcesMatchingWildcard("**/*.txt")required at least one directory level, because**did not absorb the separator after it. It now also matches a.txtresource at the classpath root.
Search for: forEachByteArray, forEachInputStream, forEachByteBuffer.
4.x code that calls forEachByteArray(consumer) from a method that already declares
throws IOException still compiles, but where it used to see an IllegalArgumentException wrapping
the cause, it now sees the IOException itself. Code that catches IllegalArgumentException around a
forEach* call must catch IOException instead. Everywhere else, the compiler will point at the
call, because IOException is checked.
An external class is one that was reached during a scan without being accepted itself — typically the superclass or an interface of an accepted class. 4.x decided whether to report external classes based on whether the class the query started from was itself external, so the same query could include or exclude them depending on where in the hierarchy it was asked. The rule in 5.x:
- A query that looks upwards — for superclasses, interfaces, annotations, meta-annotations or outer classes — includes external classes, as in 4.x. It is reporting what an accepted classfile itself declares.
- A query that looks downwards —
getAllSubclasses(),getDirectSubclasses(),getAllClassesImplementing(),getDirectClassesImplementing(),getAllSubinterfaces(),getDirectSubinterfaces(),getClassesWithAnnotation(),getClassesWithMethodAnnotation(),getClassesWithFieldAnnotation()and the rest of that family — returns only accepted classes.
Only the downward queries change. If you were relying on external classes coming back from one, call
ClassGraph#enableExternalClasses(), which returns them along with all the other external classes the
scan reached.
Classes only reachable through an external class are still found: if an accepted class extends an
external class that implements I, the accepted class is still returned by
getClassesImplementing(I). (4.x could drop these, because it filtered partway through the traversal
rather than at the end.)
Relatedly, external classes are no longer listed as members of their package or module.
PackageInfo#getClassInfo(), PackageInfo#getClassInfoRecursive() and ModuleInfo#getClassInfo()
now list only the classes that getAllClasses() returns; a package or module containing nothing but
external classes no longer appears in ScanResult#getPackageInfo() or #getModuleInfo(); and
ClassInfo#getPackageInfo() and #getModuleInfo() return null for an external class. Again,
enableExternalClasses() restores the 4.x listing.
In 4.x, the class hierarchy above an accepted class stopped as soon as it reached a JDK class, because
system modules are not scanned unless asked for. A class extending java.util.TimerTask did not
report Runnable among its interfaces, and its superclass chain ended at TimerTask. In 5.x the
classfile is read from its module even though that module is not being scanned.
What this changes for you: with enableMethodInfo(), enableFieldInfo() or
enableAnnotationInfo(), the queries that include inherited members — getMethodInfo(),
getFieldInfo(), getAnnotationInfo() and their variants — now report the members a class inherits
from JDK supertypes. An annotation type, for example, now reports annotationType(), equals(),
hashCode() and toString(), inherited from java.lang.annotation.Annotation. The
getDeclared... queries are unaffected, so if you want only what the class itself declares, use
those.
No JDK class, package or module is added to the scan result by this: the classes read this way are
external classes, so they do not appear in getAllClasses() unless enableExternalClasses() is
called. java.lang.Object is still never read, so its methods and fields are never reported.
Only reject criteria stop a classfile being read this way: a module excluded with
rejectModules() is never read from, but a module that simply was not accepted can still have
individual classfiles read out of it. This is the rule that already applied to classes. And nothing
changes at all where ClassGraph does not enumerate modules — after disableModuleScanning(), or
after overrideClasspath() or overrideClassLoaders() without the application classloader — unless
a module is asked for by name, or enableSystemJarsAndModules() is called.
Every List and Map handed back by the public API is unmodifiable. In 4.x some were and some
were not, with no way to tell from the signature — and several of the modifiable ones were
ClassGraph's own internal lists, so writing to a returned list silently corrupted the scan result.
add, remove, set, clear, sort, removeIf, replaceAll, put and the rest now throw
UnsupportedOperationException, as do the iterators, list iterators and sublists obtained from them.
This includes derived lists (filter(), union(), intersect(), exclude(), directOnly()), the
plain List returns such as InfoList#getNames() and ResourceList#getPaths(), and the lists nested
inside returned maps such as MethodInfoList#asMap() and ResourceList#asMap().
Search for: any call to a mutating method on a value that came from ClassGraph, and any
Collections.sort(...) over one. Copy first: new ArrayList<>(list), new HashMap<>(map). Lists you
construct yourself, through a public constructor such as new ClassInfoList(collection), are still
modifiable.
Note that a mutation which would not have changed the list — clear() on an empty one,
addAll(List.of()), retainAll() with a collection that already holds everything — threw in some 4.x
cases and silently did nothing in others. All of them now throw.
Empty collections are now List.of() / Set.of() rather than Collections.emptyList(). Both are
immutable and empty, but List.of() rejects a null argument to contains(), indexOf() and
lastIndexOf() with NullPointerException, where Collections.emptyList() answered false / -1.
Every public API method that does not accept null now throws NullPointerException if it is passed
one, with a message naming the parameter, e.g. packageNames[1] must not be null.
This matters most where 4.x accepted the null and carried on. About 25 methods silently ignored a null argument or returned a "not found" answer for it:
-
ClassGraph#acceptPackages,#rejectPackages,#acceptClasses,#acceptPaths,#acceptJars,#acceptModulesand the rest of the accept/reject family dropped a null varargs element and scanned with the remaining criteria — so a null in a list of package names quietly widened the scan. This is the one most likely to be hiding in a program that builds its criteria at runtime. -
ClassGraph#addClassLoader(null)and#addModuleLayer(null)were ignored. -
ScanResult#getClassInfo,#getPackageInfo,#getModuleInfo,#getResourcesWithLeafName,ClassInfo#getFieldInfo(String),#getMethodInfo(String),ClassInfoList#get(String),MethodInfoList#get(String),ResourceList#get(String),PackageInfo#getClassInfo(String)andModuleInfo#getClassInfo(String)returned null for a null name. -
ClassInfo#hasAnnotation(String),#hasDeclaredMethod(String),ClassInfoList#containsName(String)and the otherhas*/contains*queries returned false for a null name.
Search for: any ClassGraph call whose argument can be null at runtime — a value read from configuration, a map lookup, an optional field. Code that relied on passing null to mean "no filter" has to stop passing it. Code that was already passing non-null arguments is unaffected.
Comparison methods are the exception: equals(Object) and
TypeSignature#equalsIgnoringTypeParams(TypeSignature) still answer null with false.
4.x threw IllegalArgumentException for almost every kind of misuse, including many with no argument
involved. Search for: catch (IllegalArgumentException anywhere near a ClassGraph call.
-
IllegalStateExceptionwhen the failure depends on the state of the receiver: every "Please callClassGraph#enableXInfo()before#scan()" guard; using aScanResultafter it has been closed; asking a class for something it is not (getEnumConstants()on a non-enum,getAnnotationDefaultParameterValues()on a non-annotation); a classpath element, resource or module whose URI or URL cannot be determined; andTypeVariableSignature#resolve()when the declaring class was not found during the scan. -
UnsupportedOperationExceptionwhen the receiver does not support the operation at all: every mutating method of an unmodifiableInfoList;getClassName()andgetClassInfo()on the objects that do not stand for a class (AnnotationClassRef,AnnotationParameterValue,TypeArgument,TypeParameter,ClassTypeSignature,MethodTypeSignature).
IllegalArgumentException is now thrown only where an argument is present but invalid — a malformed
glob, or a type signature string that does not parse.
PackageInfo and ModuleInfo are new to the "Please call enableAnnotationInfo()" guard. In
4.x, asking a package or module for its annotations without having enabled annotation info returned an
empty list, so a forgotten call looked like a package with no annotations. It now throws. If your code
reads package or module annotations, make sure enableAnnotationInfo() (or enableAllInfo()) is
called before scan().
If you compare toString() output against stored strings, or parse it, regenerate your expected
values. ClassGraph's toString() methods are meant to read as Java source declarations, and several
had drifted from the corresponding JDK method:
-
A constructor is named after the class it constructs, not
<init>.MethodInfo#toString()printspublic com.xyz.Widget(java.lang.String a)rather thanpublic <init>(java.lang.String a).MethodInfo#getName()still returns"<init>". Static initializers are still<clinit>. -
A variadic parameter is shown as
T...inMethodParameterInfo#toString(), rather than as its declared array type. (MethodParameterInfo#isVarArgs()and#getIndex()are new.) -
A
TYPE_USEannotation on a parameter is listed once byMethodParameterInfo#toString(); it used to be printed twice. -
Annotation parameter values are rendered in the Java source syntax for their type, as
Annotation#toString()renders them:Value 4.x 5.x String[]{a, b}{"a", "b"}char[]{a, b}{'a', 'b'}byte1(byte)0x01long44Lfloat1.51.5fFloat.NaNNaN0.0f/0.0fDouble.POSITIVE_INFINITYInfinity1.0/0.0String and character escaping now covers everything the JDK escapes, rather than just the quote characters,
\nand\r. -
A field's constant initializer value is escaped the same way, in
FieldInfo#toString(). -
PackageInfo#toString()andModuleInfo#toString()name what they are —package com.xyzandmodule java.base, rather than the bare name. CallgetName()for the name alone.
getAllAnnotationInfo(annotationName) looks the name up in the list returned by
getAllAnnotationInfo(), which holds the annotations directly present on a class or member, the ones
inherited from a superclass, and the meta-annotations of both. In 4.x that list was sorted by name and
then by parameter value, so when the same annotation appeared more than once, which one you got came
down to alphabetical order of the parameter values.
The list is now sorted by name, then by how directly each annotation is related: directly present
first, then inherited, then reached as a meta-annotation. So getAllAnnotationInfo(name) returns the
annotation on the class or member itself whenever there is one. An annotation reached twice by the
same route is now listed once rather than twice.
For an array of a reference type (String[], Class[], arrays of enum constants or nested
annotations), 4.x rebuilt a fresh array on every call; arrays of primitive types were returned by
reference. Both are now returned by reference. Writing to the returned array now changes what later
calls return — copy it first if you need to modify it.
-
enableURLScheme()now rejects anything that is not a URL scheme. 4.x only checked that the argument was at least two characters, soenableURLScheme("s3:")— written with its colon, an easy mistake — was accepted and then never matched anything. The argument must now be a scheme name as defined by RFC 3986, andIllegalArgumentExceptionis thrown if it is not. -
A system module accepted by name is now scanned without
enableSystemJarsAndModules(). In 4.x,acceptModules("jdk.compiler")found nothing unlessenableSystemJarsAndModules()was also called. If your code calls both, you can drop the second one — it now means "scan all system modules", which is more than you asked for. -
The JDK's application and platform classloaders are now mapped to the scanning mechanism that can
reach their classes, for
addClassLoader()as well asoverrideClassLoaders(). Passing the application classloader now also scans the application's own modules (in 4.x it found nothing at all in an application launched on the module path); passing the platform classloader now scans the system modules rather than the application classpath. -
Classloader delegation order is now respected. In 4.x every classloader was placed in the
classpath ahead of the classloaders it delegates to, whatever its real delegation order, so for a
parent-first classloader the classpath came back in the reverse of the order the JVM resolves
classes in. This affects the order of
ScanResult#getClasspath()and friends, and which copy of a class defined in more than one classpath element is reported. It does not change anything for the JDK's own classloaders in a normal application. - A jar or directory reachable both as a module and as a classpath entry is now listed once, as the module. This is a bug fix — the methods were always documented to return the unique classpath elements — and it does not change which classes or resources a scan returns.
-
Reading from a
Resource'sInputStreamafter closing it now fails withIOException: Stream closedrather thanNullPointerException. -
Malformed classfiles are now reported rather than silently producing null names. Where 4.x
carried a null through into the scan result — producing entries whose name was null or a string
containing
"null"— 5.x throwsClassfileFormatException, which the scanner catches per classfile: the classfile is logged as invalid and skipped, and the rest of the scan proceeds. Valid classfiles are unaffected. -
ClassGraphno longer runs its reflective calls insideAccessController#doPrivileged. This only affects you if you run on JDK 17–23, explicitly opt in with-Djava.security.manager=allow, and grant ClassGraph's jar more permissions than the calling code — in which case, grant the calling code the same permissions instead. The Security Manager is permanently disabled in JDK 24 (JEP 486). -
ModuleInfo#getPackageInfo()is now unmodifiable for a module with no packages too. It used to return a fresh modifiable empty list in that one case, and an unmodifiable one otherwise. Adding to the returned list never affected the scan result, so the only change is that it now throwsUnsupportedOperationException. -
Method annotations now wrap in the class graph
.dotfile. In the node for a class, each method is a row of three cells — its annotations, its name, and its parameters — and only the parameter cell wrapped, so a class with heavily annotated methods produced a node many times wider than it needed to be. The annotation cell now wraps at the same width. The same annotations, methods and parameters are listed; only the layout changes. -
getURL()now works for system modules and jlink'd runtime images.Resource#getURL(),Resource#getClasspathElementURL(),ClassInfo#getClasspathElementURL()andResourceList#getURLs()were documented to throwIllegalStateExceptionfor ajrt:location. That has not been true since JDK 9, and the documented exception is gone. If you wrote a fallback path for it, you can delete it.
- Build the project. Fix any remaining compile errors using the tables above.
- Run your test suite.
- If any test compares against stored expected output —
.dotfiles,toString()output, ordered classpath listings, lists of subclasses or annotations — check the failures against Step 8 before regenerating the expected values, so that you are accepting an intended change rather than papering over a real one. - Turn on
ClassGraph#verbose()for one run and read the log. It reports the classpath elements in the order they will be scanned, which is the quickest way to see whether the scan is finding what it did before.
None of these are needed to port, but they are the reason for several of the changes above.
classgraph-classpath is now usable on its own, without pulling in the scanner. It reports the
classpath elements in the order the classloaders would search them, the modules split into system and
non-system, and the module path switches the JVM was launched with:
try (Classpath classpath = new ClasspathFinder().find()) {
for (ClasspathEntry entry : classpath) {
System.out.println(entry.location());
}
}ClasspathFinder has the same classloader, module layer and classpath override methods as
ClassGraph (overrideClasspath, overrideClassLoaders, addClassLoader,
ignoreParentClassLoaders, overrideModuleLayers, addModuleLayer, ignoreModules, verbose), and
finds classpath elements from the same custom container classloaders — Spring Boot, Tomcat, JBoss,
WebLogic, Quarkus and the rest.
The classpath it returns is the full, expanded classpath: the jarfiles in a classpath element's
automatic lib dirs (BOOT-INF/lib/ and WEB-INF/lib/ everywhere, plus the ones the classloader it
came from loads from, such as lib/ for Tomcat), and the entries of its manifest's Class-Path and
Bundle-ClassPath attributes, followed recursively, each reported directly after the element that
declared it. A directory is expanded the same way a jarfile is, so an exploded jarfile declares the
same children the jarfile it was exploded from did, and a child named by a relative path is resolved
beside the element that declared it, in whatever FileSystem that element lives in.
Reading those manifests means opening the classpath elements, so Classpath is AutoCloseable; the
entries can still be read after it has been closed. While it is open, those elements are open too,
so Classpath#getVfs() hands back the
virtual filesystem they were read through,
so ClasspathEntry#open(Vfs) reads a classpath element without opening it a second time.
Note that it reports where classes and resources would be loaded from, so an element that is named
but is not there is still reported, and a nested jar is reported in the outer.jar!/inner.jar form
rather than being extracted. ClassGraph#getClasspathFiles() and friends still drop the elements that
are not there and strip package roots, so they remain the ones to use when you need real files.
classgraph-vfs is a read-only virtual filesystem: the storage layer ClassGraph scans through, now
usable on its own. It reads a directory, a jarfile or a module through one interface, however the
thing was named, and it reads a jarfile nested inside another jarfile — as produced by Spring Boot and
other executable-jar formats — without extracting it to a temporary directory first:
try (Vfs vfs = new Vfs();
VfsRoot root = vfs.open("outer.jar!/lib/inner.jar")) {
for (VfsEntry entry : root) {
System.out.println(entry.getName() + " (" + entry.getLength() + " bytes)");
}
}Vfs#open accepts a path string, a File, a Path in any filesystem provider, a URI, a URL, a
ModuleReference, an InputStream, or a byte[], and returns a VfsRoot in every case. Nested
jarfiles are named by separating each jarfile from the one that encloses it with "!/", to any depth;
a trailing "!/" section naming a directory rather than a jarfile is used as the package root
("spring-boot-app.jar!/BOOT-INF/classes"), so entry names are reported relative to it. An entry's
content is read with VfsEntry#open(), #openChannel(), #read(), #load() or #loadAsString(),
and VfsRoot#asFileSystem() presents any root as a read-only java.nio.file.FileSystem, so a library
that takes a Path can read a nested jarfile or a module without knowing that is what it has.
Directory entries, encrypted entries and entries stored with an unsupported compression method are not
reported, and for a multi-release jarfile only the newest version of each entry the running JVM can
use is reported, unless the Vfs was constructed with multi-release versions enabled. The Vfs owns the file handles,
memory mappings and temporary files behind everything it opened, so it must be closed, and it must
stay open for as long as its entries are being read. It is also safe to use from many threads at once,
unlike java.util.zip.ZipFile, which serializes every public method on the instance monitor.
The scanner and the virtual filesystem are not separate worlds. Resource#getVfsEntry() returns the
VfsEntry that a scanned resource is read from, so everything above is available for a resource the
scanner found — reading it as a channel, addressing it as a Path, asking for its compressed size —
without opening anything a second time.
See the Vfs API for the full reference.
-
ScanResult#isClassInfoEnabled(),#isFieldInfoEnabled(),#isMethodInfoEnabled(),#isAnnotationInfoEnabled(),#isInterClassDependenciesEnabled(),#isExternalClassesEnabled(),#isFieldVisibilityIgnored()and#isMethodVisibilityIgnored()— so you can ask aScanResultwhat the scan was configured to collect, rather than having to pass that alongside it. -
ClassMemberInfo#getClassDependencies()(inherited byFieldInfoandMethodInfo) — the classes referred to by a single field or method. -
ClassInfoList#getAssignableTo(Class<?>)and#getAssignableTo(String), so you no longer have to look theClassInfoup first and handle the case where it was not found. -
ScanResult#getAllAnnotationsOnClass(Class)and#getDirectAnnotationsOnClass(Class), matching theClass<?>/Stringoverload pair the rest of the query API offers. -
MethodParameterInfo#isVarArgs()and#getIndex(). -
PackageInfoandModuleInfonow have the full twelve-method annotation surface, includinggetAllAnnotationInfoRepeatable(...)andgetDirectAnnotationInfoRepeatable(...).
ClassGraph's internal classes used to live under a top-level nonapi package. Each module's internals
now sit under that module's own package, in a package named internal, and are exported only to the
ClassGraph modules above them:
| Was | Is now |
|---|---|
nonapi.io.github.classgraph.utils |
io.github.classgraph.base.internal.utils |
nonapi.io.github.classgraph.reflection |
io.github.classgraph.base.internal.reflection |
nonapi.io.github.classgraph.concurrency |
io.github.classgraph.base.internal.concurrency |
nonapi.io.github.classgraph.recycler |
io.github.classgraph.base.internal.recycler |
nonapi.io.github.classgraph.fastzipfilereader |
io.github.classgraph.vfs.internal.zip |
nonapi.io.github.classgraph.fileslice |
io.github.classgraph.vfs.internal.slice |
nonapi.io.github.classgraph.fileslice.reader |
io.github.classgraph.vfs.internal.slice.reader |
nonapi.io.github.classgraph.classpath |
io.github.classgraph.classpath.internal |
nonapi.io.github.classgraph.classloaderhandler |
io.github.classgraph.classpath.internal.classloaderhandler |
nonapi.io.github.classgraph.types |
io.github.classgraph.internal.types |
nonapi.io.github.classgraph.json |
(deleted — see JSON serialization) |
Three classes did not follow the row their old package maps to. Parser and ParseException are
generic parsing machinery rather than anything to do with Java type signatures, so they are in
io.github.classgraph.base.internal.parser; only TypeUtils is left in
io.github.classgraph.internal.types. CallStackReader is a StackWalker wrapper rather than
anything to do with finding a classpath, so it is in io.github.classgraph.base.internal.reflection.
nonapi.io.github.classgraph.scanspec held the settings of a scan, which are now split across the
modules that read them: ScanSpec is in io.github.classgraph.internal.scanspec, the classpath and
classloader settings are in io.github.classgraph.classpath.internal.spec, the jarfile-reading
settings are in io.github.classgraph.vfs.internal.spec, and AcceptReject — which is glob matching
rather than a setting — is in io.github.classgraph.base.internal.utils.
Nothing in the public API refers to these packages, so this only affects code that reached into ClassGraph's internals, and OSGi or JPMS configuration that names them. If you were depending on an internal class, say so in an issue — it usually means something is missing from the public API.
Note also that Classpath is spelled with a lowercase p throughout the new classpath API —
Classpath, ClasspathEntry, ClasspathFinder — matching ClassGraph#getClasspath(),
#overrideClasspath() and the other long-standing method names.
The wiki documents the 5.x API. To read the 4.x documentation:
git clone https://github.com/classgraph/classgraph.wiki.git
cd classgraph.wiki
git checkout v4The 4.x source remains on the v4 branch of the
main repository. The last 4.x release is
4.8.190, and its Javadoc,
along with that of every earlier version, remains on
javadoc.io.
The v4 branch stays open for backwards-compatibility fixes, so 4.x code that cannot be ported yet
is not stranded.