ClassGraph loaded class missing annotations (regression)
See original GitHub issueI have an application that is functionally equivalent to the given example below. I have:
- A Spring Boot application that uses ClassGraph to find uses of an annotation.
- A Java application that uses ClassGraph to scan the Spring Boot uber-JAR.
When Java application scans my Spring Boot application, it finds the annotations via AnnotationInfo
but loadClass
gives me a Class<?>
which has no annotations.
If I use ClassGraph 4.8.66, it works. If I run my Spring Boot application independently, it also works.
I have tried with verbose
on each level and both find the annotation. I tried to compare runs on 4.8.66
and 4.8.67
but the logs were equivalent.
The example involves a bit of setup. I have setup a minimal Gradle multi-build project:
build.gradle
plugins {
id 'org.springframework.boot' version '2.3.3.RELEASE' apply false
}
subprojects {
apply plugin: 'java'
apply plugin: 'maven-publish'
repositories {
mavenLocal()
jcenter()
}
publishing {
repositories {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
}
}
sourceCompatibility = '1.11'
targetCompatibility = '1.11'
// Works:
def classGraphVersion = '4.8.66'
// Does not work (comment out the following line to test working case):
classGraphVersion = '4.8.92'
dependencies {
implementation group: 'io.github.classgraph', name: 'classgraph', version: classGraphVersion
}
}
gradle.properties
version = 1.0.0
group = com.thespiritxiii.example
settings.gradle
rootProject.name = 'example'
include 'spring'
include 'scanner'
spring/build.gradle
apply plugin: 'org.springframework.boot'
dependencies {
implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web'
}
spring/src/main/java/example/Application.java
package example;
import java.lang.annotation.Annotation;
import java.util.function.Function;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ScanResult;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
System.out.println("Annotation count: " + find());
SpringApplication.run(Application.class, args);
}
private static <A extends Annotation, T> int findGeneric(
Class<A> annotationClass,
Function<A, T> getValue
) {
int count = 0;
final ClassGraph classGraph = new ClassGraph()
.enableAllInfo()
.enableInterClassDependencies()
.enableExternalClasses();
try (ScanResult scanResult = classGraph.enableAnnotationInfo().scan()) {
for (ClassInfo classInfo : scanResult.getClassesWithAnnotation(annotationClass.getName())) {
final Class<?> classType = classInfo.loadClass();
final A mixin = classType.getAnnotation(annotationClass);
// This should never be null because I called getClassesWithAnnotation with the same class.
if (mixin == null) {
throw new RuntimeException("Unable to find annotation on class: " + classType);
}
final T value = getValue.apply(mixin);
if (value != null) {
count += 1;
}
}
}
return count;
}
public static final int find() {
return findGeneric(SomeAnnotation.class, SomeAnnotation::value);
}
}
spring/src/main/java/example/SomeAnnotation.java
package example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SomeAnnotation {
String value();
}
spring/src/main/java/example/SomeAnnotatedClass.java
package example;
@SomeAnnotation("String")
public class SomeAnnotatedClass {
}
scanner/build.gradle
apply plugin: 'application'
application {
mainClassName = 'example.App'
}
dependencies {
// Spring Boot builds as an Uber jar so we don't need transitive dependencies.
implementation(group: 'com.thespiritxiii.example', name: 'spring', version: project.version) {
transitive = false
}
}
scanner/src/main/java/example/App.java
package example;
import java.lang.reflect.Method;
import java.util.concurrent.ForkJoinPool;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ScanResult;
public class App {
public static void main(String[] args) {
final ClassGraph classGraph = new ClassGraph()
.enableAllInfo()
.enableExternalClasses()
.enableInterClassDependencies();
// Run with 1 core (deterministically).
try (ScanResult scanResult = classGraph.scanAsync(ForkJoinPool.commonPool(), 1).get()) {
final Class<?> classType = scanResult.getClassInfo("example.Application").loadClass();
final Method method = classType.getMethod("find");
final Object value = method.invoke(null);
System.out.println("Value is: " + value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
To test:
./gradlew spring:bootRun
Outputs 1.
./gradlew spring:build spring:publishToMavenLocal scanner:run
Throws an exception if ClassGraph is 4.8.67 or above. If 4.8.66, outputs 1.
After testing, delete ~/.m2/repository/com/thespiritxiii/example/spring
from the maven local publish task.
Issue Analytics
- State:
- Created 3 years ago
- Comments:14 (9 by maintainers)
Top GitHub Comments
Thank you for your through investigation and prompt replies! That solution should work for me. 😃
So
.overrideClassLoaders(Application.class.getClassLoader())
is now supported as an easier way to solve the problem you ran into.Note however that sharing a
ClassGraphClassLoader
between nested scans is not advisable, because scan criteria may differ between the outer scan and the inner scan.If you do override the classloader in this way, the outer nested scan’s classloader will be deferred to first, followed by attempting to load classes from the inner nested scan’s classloader. This should work to an arbitrary nesting depth.
You’ll need to modify your code using one of the methods I showed above, but this is fixed in 4.8.93.
Thanks for the bug report, and for being responsive with answers!