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.
@Retention(RUNTIME)
@Target(TYPE)
public @interface BusinessProcessor {
}
@Retention(RUNTIME)
@Target(TYPE)
public @interface Smart {
}
@Retention(RUNTIME)
@Target(TYPE)
public @interface Traditional {
}
@Retention(RUNTIME)
@Target({METHOD, FIELD})
public @interface AppConfig {
}
Following are Java classes annotated with annotations we defined above.
public interface Processor {
void process();
}
@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");
}
}
@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
- Creating new instance of ClassGraph class
- Setting up to scan classes in com.readtorakesh.reflection package and all of it’s sub packages.
- Enabling verbose log for debugging
- 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.
- 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.
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
- Symmetric Encryption with AES in Java 8
- Thread Concurrency using ExecutorService in Java 8
- Java Thread Local Storage explained in easiest practical way
There is a better classpath scanner: take a look here
https://jim-van-hell.medium.com/scanning-the-classpaths-in-java-1348315fd803
Thanks for suggesting alternate option.