Skip to content
Luke Hutchison edited this page Oct 5, 2021 · 105 revisions

See also the ClassGraph API overview.

Contents

Scanning classfiles

Configuring and starting the scan

After instantiating a ClassGraph instance, you configure the instance for scanning. Two of the main configuration steps are to call .enableAllInfo() if you want to scan classfiles (without this, you will only get Resource results), and to call .acceptPackages(String... packageName) to accept the packages you want to scan (without accepting packages, all packages are scanned). You can also call .verbose() to log to stderr for debugging purposes.

You then call .scan() to start the scan. This returns a ScanResult instance, which you can query for classes of interest, producing a ClassInfoList of ClassInfo objects.

The ScanResult should be assigned in a try-with-resources block, or manually closed when it is no longer needed.

try (ScanResult scanResult =                // Assign scanResult in try-with-resources
        new ClassGraph()                    // Create a new ClassGraph instance
            .verbose()                      // If you want to enable logging to stderr
            .enableAllInfo()                // Scan classes, methods, fields, annotations
            .acceptPackages("com.xyz")      // Scan com.xyz and subpackages
            .scan()) {                      // Perform the scan and return a ScanResult
    // Use the ScanResult within the try block, e.g.
    ClassInfo widgetClassInfo = scanResult.getClassInfo("com.xyz.Widget");
    // ...
}

Find classes annotated with com.xyz.Route

From a ScanResult, you can query for classes annotated with a given annotation, producing a ClassInfoList, consisting of ClassInfo objects for the annotated classes. Notably this is accomplished by reading the class bytecode directly, without the classloader loading or initializing the class. The annotations on these classes can then be obtained as an AnnotationInfoList:

try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
            .scan()) {
    ClassInfoList routeClassInfoList = scanResult.getClassesWithAnnotation("com.xyz.Route");
    for (ClassInfo routeClassInfo : routeClassInfoList) {
        // Get the Route annotation on the class
        AnnotationInfo annotationInfo = routeClassInfo.getAnnotationInfo("com.xyz.Route");
        AnnotationParameterValueList paramVals = annotationInfo.getParameterValues();

        // The Route annotation has a parameter named "path"
        String routePath = paramVals.get("path");

        // Alternatively, you can load and instantiate the annotation, so that the annotation
        // methods can be called directly to get the annotation parameter values (this sets up
        // an InvocationHandler to emulate the Route annotation instance, since annotations
        // can't be instantiated directly without also loading the annotated class).
        Route route = (Route) annotationInfo.loadClassAndInstantiate();
        String routePathDirect = route.path();

        // ...
    }
}

Find the names of classes that implement interface com.xyz.Widget

For convenience, you can get a list of class names from a ClassInfoList by calling the .getNames() method:

try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
            .scan()) {
    ClassInfoList widgetClasses = scanResult.getClassesImplementing("com.xyz.Widget");
    List<String> widgetClassNames = widgetClasses.getNames();
    // ...
}

Get Class<?> references for subclasses of com.xyz.Control

You can get Class<?> references from ClassInfo objects by calling the .loadClass() method, or from a ClassInfoList by calling the .loadClasses() method:

🛑 N.B. You should do all classloading through ClassGraph, using ClassInfo#loadClass() or ClassInfoList#loadClasses(), and never using Class.forName(className), otherwise you may end up with some classes loaded by the context classloader, and some by another classloader (ClassGraph may need to create its own custom classloader to load classes from some jarfiles). This can cause ClassCastException or other problems at weird places in your code.

try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
            .scan()) {
    ClassInfoList controlClasses = scanResult.getSubclasses("com.xyz.Control");
    List<Class<?>> controlClassRefs = controlClasses.loadClasses();
    // ...
}

Find methods, fields, and annotations for class com.xyz.Form

From a ClassInfo object, you can fetch a MethodInfoList of MethodInfo objects, a FieldInfoList of FieldInfo objects, and/or an AnnotationInfoList of AnnotationInfo objects, which respectively provide information about the methods, fields and annotations of the class. Again this is accomplished without loading or initializing the class.

try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
            .scan()) {
    ClassInfo form = scanResult.getClassInfo("com.xyz.Form");
    if (form != null) {
        MethodInfoList formMethods = form.getMethodInfo();
        for (MethodInfo mi : formMethods) {
            String methodName = mi.getName();
            MethodParameterInfo[] mpi = mi.getParameterInfo();
            for (int i = 0; i < mpi.length; i++) {
                String parameterName = mpi[i].getName();
                TypeSignature parameterType = 
                        mpi[i].getTypeSignatureOrTypeDescriptor();
                // ...
            }
        } 
        FieldInfoList formFields = form.getFieldInfo();
        for (FieldInfo fi : formFields) {
            String fieldName = fi.getName();
            TypeSignature fieldType = fi.getTypeSignatureOrTypeDescriptor();
            // ...
        }
        AnnotationInfoList formAnnotations = form.getAnnotationInfo();
        for (AnnotationInfo ai : formAnnotations) {
            String annotationName = ai.getName();
            List<AnnotationParameterValue> annotationParamVals = 
                    ai.getParameterValues();
            // ...
        }
    }
}

Find subclasses of com.xyz.Box with annotation com.xyz.Checked

This is compound query that requires a Boolean "and" operator. There is more than one way to accomplish this:

(1) Using ClassInfo#intersect(ClassInfo):

ClassInfoList provides union ("and"), intersection ("or"), and set difference / exclusion ("and-not") operators:

try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
            .scan()) {
    ClassInfoList boxes = scanResult.getSubclasses("com.xyz.Box");
    ClassInfoList checked = scanResult.getClassesWithAnnotation("com.xyz.Checked");
    ClassInfoList checkedBoxes = boxes.intersect(checked);
    // ...
}

(2) Using ClassInfo#filter(ClassInfoFilter):

Equivalently, you could also apply a filter predicate to a ClassInfoList to produce a new ClassInfoList contanining a selected subset of elements:

try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
            .scan()) {
    ClassInfoList checkedBoxes = scanResult.getSubclasses("com.xyz.Box")
            .filter(classInfo -> classInfo.hasAnnotation("com.xyz.Checked"));
    // ...
}

Filtering classes using compound criteria

Filter criteria can be arbitrarily complex, for example:

try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
            .scan()) {
    ClassInfoList filtered = scanResult.getAllClasses()
            .filter(classInfo ->
                (classInfo.isInterface() || classInfo.isAbstract())
                    && classInfo.hasAnnotation("com.xyz.Widget")
                    && classInfo.hasMethod("open"));
    // ...
}

Note that some of the ClassInfo predicate methods do not take a parameter, so they can also be used directly as function references in place of a ClassInfoFilter, e.g.:

ClassInfoList interfaces = classInfoList.filter(ClassInfo::isInterface);

Find the direct subclasses of com.xyz.Box

The ClassInfoList#directOnly() method restricts a list of ClassInfo objects to only those in direct relationship, given the relationship type that produced the ClassInfoList. For example, to restrict the list of subclasses of com.xyz.Box to just the direct subclasses, and not subclasses of the subclasses:

try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
            .scan()) {
    ClassInfoList directBoxes = scanResult.getSubclasses("com.xyz.Box").directOnly();
    // ...
}

Find all meta-annotations

A meta-annotation annotates an annotation. For the ClassInfo of an annotation class, ci, the classes it annotates can be found by calling ci.getClassesWithAnnotation(), returning a ClassInfoList. That list can be filtered for only annotation classes by then calling .getAnnotations(), returning the list of classes annotated by ci that are themselves annotations. Checking if that list is empty tests whether or not ci is a meta-annotation:

try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
            .scan()) {
    ClassInfoList metaAnnotations = scanResult.getAllAnnotations()
            .filter(ci -> !ci.getClassesWithAnnotation().getAnnotations().isEmpty());
    // ...
}

Reading type annotations

Annotations (optionally with parameters) can be added to types in Java. The following example prints 100, which is read from the type argument annotation @Size(100) on the field List<@Size(100) String> values:

public class TypeAnnotation {
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Size {
        int value();
    }

    public class Test {
        List<@Size(100) String> values;
    }

    public static void main(String[] args) {
        try (ScanResult scanResult = new ClassGraph()
                .acceptPackages(TypeAnnotation.class.getPackage().getName())
                .enableAllInfo()
                .scan()) {
            ClassInfo ci = scanResult.getClassInfo(Test.class.getName());
            FieldInfo fi = ci.getFieldInfo().get(0);
            ClassRefTypeSignature ts = (ClassRefTypeSignature) fi.getTypeSignature();
            List<TypeArgument> taList = ts.getTypeArguments();
            TypeArgument ta = taList.get(0);
            ReferenceTypeSignature taSig = ta.getTypeSignature();
            AnnotationInfoList aiList = taSig.getTypeAnnotationInfo();
            AnnotationInfo ai = aiList.get(0);
            AnnotationParameterValueList apVals = ai.getParameterValues();
            AnnotationParameterValue apVal = apVals.get(0);
            int size = (int) apVal.getValue();
            System.out.println(size);
        }
    }
}

Scanning specific URLs

Rather than scanning all detected classloaders and modules, you can scan specific URLs by calling either .overrideClassLoaders(new URLClassLoader(urls)) or simply .overrideClasspath(urls) before .scan():

public void scan(URL[] urls) {
    try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
                .overrideClassLoaders(new URLClassLoader(urls))
                .scan()) {
        // ...
    }
}

or

public void scan(String pathToScan) {
    try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
                .overrideClasspath(pathToScan)
                .scan()) {
        // ...
    }
}

Finding and reading resource files

Resources (files) in the classpath or module path can be obtained as a ResourceList of Resource objects. The content of any Resource can be opened or read as an InputStream, ByteBuffer, or byte[] array.

Note that you should close Resource objects after reading from them.

Read the content of all XML resource files in META-INF/config

This is a different type of query that finds resources with a matching file path, rather than classes based on class properties.

If you only need to scan resources and not classes, .enableClassInfo() or .enableAllInfo() should not be called, for speed. Also, if you don't need to scan classes, you should specify the accept by calling .acceptPaths() and using path separators (/), rather than by calling .acceptPackages() and using package separators (.). Path and package accepts work the same way internally, you can just choose one way or the other of specifying the accept/reject. However, calling .acceptPackages() also implicitly calls .enableClassInfo().

Map<String, String> pathToFileContent = new HashMap<>();
try (ScanResult scanResult = new ClassGraph().acceptPaths("META-INF/config").scan()) {
    scanResult.getResourcesWithExtension("xml")
            .forEachByteArray((Resource res, byte[] fileContent) -> {
                pathToFileContent.put(
                        res.getPath(), new String(fileContent, StandardCharsets.UTF_8));
            });
}

There are several methods in ScanResult for getting resources matching given criteria:

  • .getAllResources()
  • .getResourcesWithPath(String resourcePath)
  • .getResourcesWithLeafName(String leafName)
  • .getResourcesWithExtension(String extension)
  • .getResourcesMatchingPattern(Pattern pattern)

Find all duplicate class definitions in the classpath or module path

It can be useful to know when the same class is defined multiple times in the classpath or module path.

In the ScanResult, ClassGraph only returns one ClassInfo object for any given fully-qualified class name, which corresponds to the first instance of the class encountered in the classpath or module path (any later definitions of the same class are ignored, in order to emulate the "masking" or "shadowing" semantics of the JRE). However, ScanResult#getAllResources() returns a ResourceList that includes Resource objects for both non-classfiles and classfiles (since a classfile is technically a resource).

Calling ResourceList#classFilesOnly() returns another ResourceList consisting of only the Resource elements whose path ends in ".class".

Calling ResourceList#findDuplicatePaths() returns a List<Entry<String, ResourceList>> where the entry key is the path, and the entry value is a ResourceList consisting of two or more Resource objects for the duplicated resources.

Therefore, you can print the classpath / module path URLs for all duplicated classfiles as follows:

for (Entry<String, ResourceList> dup :
        new ClassGraph().scan()
            .getAllResources()
            .classFilesOnly()                        // Remove this for all resource types
            .findDuplicatePaths()) {
    System.out.println(dup.getKey());                // Classfile path
    for (Resource res : dup.getValue()) {
        System.out.println(" -> " + res.getURI());   // Print Resource URI
    }
}