Java Classpath Scanning using ClassGraph

Java Classpath Scanning using ClassGraph

The standard Java Class API or Reflection API allows us to find all interfaces/classes implemented/extended by a class/interface. It also allows us to find all annotations of a class, field or method. What if we want to do it in reverse order like in the classpath, find all classes which implements an interface or extends a class, final all classes annotated with certain annotation or to make it more complex find classes which has a field/method annotated with certain annotation.

This is where classpath scanner libraries come to rescue and one of them is ClassGraph. I like ClassGraph because it performs scanning of classes at byte-code level with parallelism which makes it uber-fast, ultra-lightweight, parallelized classpath scanner and module scanner for Java, Scala, Kotlin and other JVM languages. In this article we will see Java Classpath Scanning using ClassGraph. ClassGraph is fully compatible with the new JPMS module system (Project Jigsaw / JDK 9+), i.e. it can scan both the traditional classpath and the module path, however, the code is also fully backwards compatible with JDK 7 and JDK 8.

That’s basics of ClassGraph. Lets see practical examples and usage of ClassGraph.

Gradle Dependency

compile group: 'io.github.classgraph', name: 'classgraph', version: '4.8.46'

Annotations and Domain Classes

We will use following annotations in our example.

BusinessProcessor.java
@Retention(RUNTIME)
@Target(TYPE)
public @interface BusinessProcessor {

}
Smart.java
@Retention(RUNTIME)
@Target(TYPE)
public @interface Smart {

}
Traditional.java
@Retention(RUNTIME)
@Target(TYPE)
public @interface Traditional {

}
AppConfig.java
@Retention(RUNTIME)
@Target({METHOD, FIELD})
public @interface AppConfig {

}

Following are Java classes annotated with annotations we defined above.

Processor.java
public interface Processor {
    void process();
}
SmartProcessor.java
@BusinessProcessor
@Smart
public class SmartProcessor implements Processor {
    
    // Smart, auto injected properties
    @AppConfig
    private Properties properties;
    
    @Override
    public void process() {
        System.out.println("This is modern processor");
    }

}
TraditionalProcessor.java
@BusinessProcessor
@Traditional
public class TraditionalProcessor implements Processor {

    private Properties properties;
    
    @AppConfig
    public void setProperties() {
        // Manually load properties
    }
    
    @Override
    public void process() {
        System.out.println("This is modern processor");
    }

}

Setup Scanner and Do Scan

ScanResult scanResult = new ClassGraph()
        .whitelistPackages("com.readtorakesh.reflection") 
        .verbose()
        .enableAllInfo() 
        .scan();

We are doing following things in one statement

  1. Creating new instance of ClassGraph class
  2. Setting up to scan classes in com.readtorakesh.reflection package and all of it’s sub packages.
  3. Enabling verbose log for debugging
  4. Telling scanner to parse all information of scanned classes. We can be specific and capture only required information ex. Just class information, just field information or just annotation information.
  5. Perform the scanning.

Lets use scan result to find classes matching certain criteria.

Find all classes which implements Processor interface

ClassInfoList classInfoList = scanResult.getClassesImplementing(Processor.class.getName());

We can easily iterate over ClassInfoList object to get each matching class.

for (ClassInfo classInfo : classInfoList) {
    System.out.println("\t" + classInfo.getName());
}

Classes annotated with @BusinessProcessor annotation

scanResult.getClassesWithAnnotation(BusinessProcessor.class.getName());

Classes annotated with @Smart annotation

scanResult.getClassesWithAnnotation(Smart.class.getName());

Classes annotated with @Traditional annotation

scanResult.getClassesWithAnnotation(Traditional.class.getName());

Classes having field annotated with @AppConfig annotation

scanResult.getClassesWithFieldAnnotation(AppConfig.class.getName());

Classes having method annotated with @AppConfig annotation

scanResult.getClassesWithMethodAnnotation(AppConfig.class.getName());

Here is main program showing usage of above methods along with output.

MainApp.java
package com.readtorakesh.java.classgraph;

import com.readtorakesh.reflection.annotation.AppConfig;
import com.readtorakesh.reflection.annotation.BusinessProcessor;
import com.readtorakesh.reflection.annotation.Smart;
import com.readtorakesh.reflection.annotation.Traditional;
import com.readtorakesh.reflection.processor.Processor;

import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;

public class MainApp {

    public static void main(String[] args) {
        ScanResult scanResult = new ClassGraph()
                .whitelistPackages("com.readtorakesh.reflection") 
                .verbose()
                .enableAllInfo() 
                .scan();
        
        ClassInfoList classInfoList = null;

        System.out.println("Classes which implements '" + Processor.class.getSimpleName() + "' interface");
        classInfoList = scanResult.getClassesImplementing(Processor.class.getName());
        for (ClassInfo classInfo : classInfoList) {
            System.out.println("\t" + classInfo.getName());
        }

        System.out.println("\nClasses annotated with '@" + BusinessProcessor.class.getSimpleName() + "' annotation");
        classInfoList = scanResult.getClassesWithAnnotation(BusinessProcessor.class.getName());
        for (ClassInfo classInfo : classInfoList) {
            System.out.println("\t" + classInfo.getName());
        }

        System.out.println("\nClasses annotated with '@" + Smart.class.getSimpleName() + "' annotation");
        classInfoList = scanResult.getClassesWithAnnotation(Smart.class.getName());
        for (ClassInfo classInfo : classInfoList) {
            System.out.println("\t" + classInfo.getName());
        }

        System.out.println("\nClasses annotated with '@" + Traditional.class.getSimpleName() + "' annotation");
        classInfoList = scanResult.getClassesWithAnnotation(Traditional.class.getName());
        for (ClassInfo classInfo : classInfoList) {
            System.out.println("\t" + classInfo.getName());
        }

        System.out
                .println("\nClasses having field annotated with '@" + AppConfig.class.getSimpleName() + "' annotation");
        classInfoList = scanResult.getClassesWithFieldAnnotation(AppConfig.class.getName());
        for (ClassInfo classInfo : classInfoList) {
            System.out.println("\t" + classInfo.getName());
        }

        System.out.println(
                "\nClasses having method annotated with '@" + AppConfig.class.getSimpleName() + "' annotation");
        classInfoList = scanResult.getClassesWithMethodAnnotation(AppConfig.class.getName());
        for (ClassInfo classInfo : classInfoList) {
            System.out.println("\t" + classInfo.getName());
        }

    }
}
/*
--- Output ---
Classes which implements 'Processor' interface
    com.readtorakesh.reflection.processor.SmartProcessor
    com.readtorakesh.reflection.processor.TraditionalProcessor

Classes annotated with '@BusinessProcessor' annotation
    com.readtorakesh.reflection.processor.SmartProcessor
    com.readtorakesh.reflection.processor.TraditionalProcessor

Classes annotated with '@Smart' annotation
    com.readtorakesh.reflection.processor.SmartProcessor

Classes annotated with '@Traditional' annotation
    com.readtorakesh.reflection.processor.TraditionalProcessor

Classes having field annotated with '@AppConfig' annotation
    com.readtorakesh.reflection.processor.SmartProcessor

Classes having method annotated with '@AppConfig' annotation
    com.readtorakesh.reflection.processor.TraditionalProcessor
*/

Caution on creating instance of found classes

When we want to instantiate found classes, it’s very important to do that not via Class.forName but by using the library method ClassInfo.loadClass.

The reason is that Classgraph uses its own class loader to load classes from some JAR files. So, if we use Class.forName, the same class might be loaded more than once by different class loaders, and this might lead to non-trivial bugs.

Download Code

You can download complete source code referred in this blog with gradle project from github. https://github.com/rakeshprajapati1982/classgraph

Please share it and help others if you found this blog helpful. Feedback, questions and comments are always welcome.

Reference

https://github.com/classgraph/classgraph

Further Reading

 

3 Comments

Comments

%d bloggers like this: