Java Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this brief tutorial, we'll talk about the Classgraph library — what it helps with and how we can use it.

Classgraph helps us to find target resources in the Java classpath, builds metadata about the resources found and provides convenient APIs for working with the metadata.

This use-case is very popular in Spring-based applications, where components marked with stereotype annotations are automatically registered in the application context. However, we can exploit that approach for custom tasks as well. For example, we might want to find all classes with a particular annotation, or all resource files with a certain name.

The cool thing is that Classgraph is fast, as it works on the byte-code level, meaning the inspected classes are not loaded to the JVM, and it doesn't use reflection for processing.

2. Maven Dependencies

First, let's add the classgraph library to our pom.xml:

<dependency>
    <groupId>io.github.classgraph</groupId>
    <artifactId>classgraph</artifactId>
    <version>4.8.28</version>
</dependency>

In the next sections, we'll look into several practical examples with the library's API.

3. Basic Usage

There are three basic steps to using the library:

  1. Set up scan options – for example, target package(s)
  2. Perform the scan
  3. Work with the scan results

Let's create the following domain for our example setup:

@Target({TYPE, METHOD, FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {

    String value() default "";
}
@TestAnnotation
public class ClassWithAnnotation {
}

Now let's see the 3 steps above on an example of looking for classes with the @TestAnnotation:

try (ScanResult result = new ClassGraph().enableClassInfo().enableAnnotationInfo()
  .whitelistPackages(getClass().getPackage().getName()).scan()) {
    
    ClassInfoList classInfos = result.getClassesWithAnnotation(TestAnnotation.class.getName());
    
    assertThat(classInfos).extracting(ClassInfo::getName).contains(ClassWithAnnotation.class.getName());
}

Let's break down the example above:

  • we started by setting up the scan options (we've configured the scanner to parse only class and annotation info, as well instructing it to parse only files from the target package)
  • we performed the scan using the ClassGraph.scan() method
  • we used the ScanResult to find annotated classes by calling the getClassWithAnnotation() method

As we'll also see in the next examples, the ScanResult object can contain a lot of information about the APIs we want to inspect, such as the ClassInfoList.

4. Filtering by Method Annotation

Let's expand our example to method annotations:

public class MethodWithAnnotation {

    @TestAnnotation
    public void service() {
    }
}

We can find all classes that have methods marked by the target annotation using a similar method — getClassesWithMethodAnnotations():

try (ScanResult result = new ClassGraph().enableAllInfo()
  .whitelistPackages(getClass().getPackage().getName()).scan()) {
    
    ClassInfoList classInfos = result.getClassesWithMethodAnnotation(TestAnnotation.class.getName());
    
    assertThat(classInfos).extracting(ClassInfo::getName).contains(MethodWithAnnotation.class.getName());
}

The method returns a ClassInfoList object containing information about the classes that match the scan.

5. Filtering by Annotation Parameter

Let's also see how we can find all classes with methods marked by the target annotation and with a target annotation parameter value.

First, let's define classes containing methods with the @TestAnnotation, with 2 different parameter values:

public class MethodWithAnnotationParameterDao {

    @TestAnnotation("dao")
    public void service() {
    }
}
public class MethodWithAnnotationParameterWeb {

    @TestAnnotation("web")
    public void service() {
    }
}

Now, let's iterate through the ClassInfoList result, and verify each method's annotations:

try (ScanResult result = new ClassGraph().enableAllInfo()
  .whitelistPackages(getClass().getPackage().getName()).scan()) {

    ClassInfoList classInfos = result.getClassesWithMethodAnnotation(TestAnnotation.class.getName());
    ClassInfoList webClassInfos = classInfos.filter(classInfo -> {
        return classInfo.getMethodInfo().stream().anyMatch(methodInfo -> {
            AnnotationInfo annotationInfo = methodInfo.getAnnotationInfo(TestAnnotation.class.getName());
            if (annotationInfo == null) {
                return false;
            }
            return "web".equals(annotationInfo.getParameterValues().getValue("value"));
        });
    });

    assertThat(webClassInfos).extracting(ClassInfo::getName)
      .contains(MethodWithAnnotationParameterWeb.class.getName());
}

Here, we've used the AnnotationInfo and MethodInfo metadata classes to find metadata on the methods and annotations we want to check.

6. Filtering by Field Annotation

We can also use the getClassesWithFieldAnnotation() method to filter a ClassInfoList result based on field annotations:

public class FieldWithAnnotation {

    @TestAnnotation
    private String s;
}
try (ScanResult result = new ClassGraph().enableAllInfo()
  .whitelistPackages(getClass().getPackage().getName()).scan()) {

    ClassInfoList classInfos = result.getClassesWithFieldAnnotation(TestAnnotation.class.getName());
 
    assertThat(classInfos).extracting(ClassInfo::getName).contains(FieldWithAnnotation.class.getName());
}

7. Finding Resources

Finally, we'll have a look at how we can find information on classpath resources.

Let's create a resource file in the classgraph classpath root directory — for example, src/test/resources/classgraph/my.config — and give it some content:

my data

We can now find the resource and get its contents:

try (ScanResult result = new ClassGraph().whitelistPaths("classgraph").scan()) {
    ResourceList resources = result.getResourcesWithExtension("config");
    assertThat(resources).extracting(Resource::getPath).containsOnly("classgraph/my.config");
    assertThat(resources.get(0).getContentAsString()).isEqualTo("my data");
}

We can see here we've used the ScanResult's getResourcesWithExtension() method to look for our specific file. The class has a few other useful resource-related methods, such as getAllResources(), getResourcesWithPath() and getResourcesMatchingPattern().

These methods return a ResourceList object, which can be further used to iterate through and manipulate Resource objects.

8. Instantiation

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.

9. Conclusion

In this article, we learned how to effectively find classpath resources and inspect their contents with the Classgraph library.

As usual, the complete source code for this article is available over on GitHub.

Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!