Skip to content

Commit d62b36c

Browse files
committed
use a more light-weight method for class scanning
1 parent 5000993 commit d62b36c

7 files changed

Lines changed: 167 additions & 25 deletions

File tree

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,33 @@
11
package it.aboutbits.springboot.toolbox.reflection.util;
22

3-
import io.github.classgraph.ClassGraph;
4-
import io.github.classgraph.ClassInfo;
5-
import io.github.classgraph.ScanResult;
3+
import lombok.SneakyThrows;
64
import org.jspecify.annotations.NullMarked;
5+
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
6+
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
7+
import org.springframework.core.type.filter.AnnotationTypeFilter;
8+
import org.springframework.core.type.filter.AssignableTypeFilter;
79

810
import java.lang.annotation.Annotation;
911
import java.util.Arrays;
10-
import java.util.Map;
12+
import java.util.Objects;
1113
import java.util.Set;
12-
import java.util.concurrent.ConcurrentHashMap;
1314
import java.util.stream.Collectors;
1415

1516
@NullMarked
1617
public final class ClassScannerUtil {
17-
private static final Map<String, ClassScanner> CACHE = new ConcurrentHashMap<>();
1818

1919
private ClassScannerUtil() {
2020
}
2121

2222
public static ClassScanner getScannerForPackages(String... packages) {
23-
var cacheKey = Arrays.stream(packages)
24-
.sorted()
25-
.collect(Collectors.joining("|"));
26-
return CACHE.computeIfAbsent(cacheKey, _ -> new ClassScanner(packages));
23+
return new ClassScanner(packages);
2724
}
2825

2926
public static final class ClassScanner {
30-
private final ScanResult scanResult;
3127
private final String[] packages;
3228

3329
private ClassScanner(String... packages) {
3430
this.packages = packages;
35-
this.scanResult = new ClassGraph()
36-
.enableAllInfo()
37-
.acceptPackages(packages)
38-
.scan();
3931
}
4032

4133
public String[] getScannedPackages() {
@@ -44,20 +36,36 @@ public String[] getScannedPackages() {
4436

4537
@SuppressWarnings("unchecked")
4638
public <T> Set<Class<? extends T>> getSubTypesOf(Class<T> clazz) {
47-
var classInfoList = clazz.isInterface()
48-
? scanResult.getClassesImplementing(clazz)
49-
: scanResult.getSubclasses(clazz);
50-
return classInfoList.loadClasses()
51-
.stream()
52-
.map(item -> (Class<? extends T>) item)
39+
var scanner = createScanner();
40+
scanner.addIncludeFilter(new AssignableTypeFilter(clazz));
41+
return Arrays.stream(packages)
42+
.flatMap(pkg -> scanner.findCandidateComponents(pkg).stream())
43+
.map(bd -> (Class<? extends T>) loadClass(Objects.requireNonNull(bd.getBeanClassName())))
44+
.filter(c -> !c.equals(clazz))
5345
.collect(Collectors.toSet());
5446
}
5547

5648
public Set<Class<?>> getClassesAnnotatedWith(Class<? extends Annotation> clazz) {
57-
var result = scanResult.getClassesWithAnnotation(clazz);
58-
return result.stream().map(
59-
ClassInfo::loadClass
60-
).collect(Collectors.toSet());
49+
var scanner = createScanner();
50+
scanner.addIncludeFilter(new AnnotationTypeFilter(clazz));
51+
return Arrays.stream(packages)
52+
.flatMap(pkg -> scanner.findCandidateComponents(pkg).stream())
53+
.map(bd -> loadClass(Objects.requireNonNull(bd.getBeanClassName())))
54+
.collect(Collectors.toSet());
55+
}
56+
57+
private static ClassPathScanningCandidateComponentProvider createScanner() {
58+
return new ClassPathScanningCandidateComponentProvider(false) {
59+
@Override
60+
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
61+
return true;
62+
}
63+
};
64+
}
65+
66+
@SneakyThrows(ClassNotFoundException.class)
67+
private static Class<?> loadClass(String className) {
68+
return Class.forName(className);
6169
}
6270
}
6371
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package it.aboutbits.springboot.toolbox.reflection.util;
2+
3+
import it.aboutbits.springboot.toolbox.reflection.util.testfixtures.ScanTestAbstractImpl;
4+
import it.aboutbits.springboot.toolbox.reflection.util.testfixtures.ScanTestAnnotatedClass;
5+
import it.aboutbits.springboot.toolbox.reflection.util.testfixtures.ScanTestAnnotation;
6+
import it.aboutbits.springboot.toolbox.reflection.util.testfixtures.ScanTestConcreteImpl;
7+
import it.aboutbits.springboot.toolbox.reflection.util.testfixtures.ScanTestInterface;
8+
import org.jspecify.annotations.NullMarked;
9+
import org.junit.jupiter.api.DisplayName;
10+
import org.junit.jupiter.api.Nested;
11+
import org.junit.jupiter.api.Test;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
@NullMarked
16+
class ClassScannerUtilTest {
17+
private static final String FIXTURES_PACKAGE = "it.aboutbits.springboot.toolbox.reflection.util.testfixtures";
18+
private static final String OTHER_PACKAGE = "it.aboutbits.springboot.toolbox.util";
19+
20+
@Nested
21+
class GetSubTypesOf {
22+
@Test
23+
@DisplayName("Should find concrete and abstract subtypes of an interface")
24+
void shouldFindConcreteAndAbstractSubtypesOfInterface() {
25+
// given
26+
var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE);
27+
28+
// when
29+
var result = scanner.getSubTypesOf(ScanTestInterface.class);
30+
31+
// then
32+
assertThat(result).containsExactlyInAnyOrder(ScanTestConcreteImpl.class, ScanTestAbstractImpl.class);
33+
}
34+
35+
@Test
36+
@DisplayName("Should not include the target interface itself")
37+
void shouldNotIncludeTargetInterfaceItself() {
38+
// given
39+
var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE);
40+
41+
// when
42+
var result = scanner.getSubTypesOf(ScanTestInterface.class);
43+
44+
// then
45+
assertThat(result).doesNotContain(ScanTestInterface.class);
46+
}
47+
48+
@Test
49+
@DisplayName("Should return empty set when no subtypes exist in scanned package")
50+
void shouldReturnEmptySetWhenNoSubtypesExistInScannedPackage() {
51+
// given
52+
var scanner = ClassScannerUtil.getScannerForPackages(OTHER_PACKAGE);
53+
54+
// when
55+
var result = scanner.getSubTypesOf(ScanTestInterface.class);
56+
57+
// then
58+
assertThat(result).isEmpty();
59+
}
60+
}
61+
62+
@Nested
63+
class GetClassesAnnotatedWith {
64+
@Test
65+
@DisplayName("Should find classes annotated with the given annotation")
66+
void shouldFindClassesAnnotatedWithGivenAnnotation() {
67+
// given
68+
var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE);
69+
70+
// when
71+
var result = scanner.getClassesAnnotatedWith(ScanTestAnnotation.class);
72+
73+
// then
74+
assertThat(result).containsExactly(ScanTestAnnotatedClass.class);
75+
}
76+
77+
@Test
78+
@DisplayName("Should return empty set when no annotated classes exist in scanned package")
79+
void shouldReturnEmptySetWhenNoAnnotatedClassesExistInScannedPackage() {
80+
// given
81+
var scanner = ClassScannerUtil.getScannerForPackages(OTHER_PACKAGE);
82+
83+
// when
84+
var result = scanner.getClassesAnnotatedWith(ScanTestAnnotation.class);
85+
86+
// then
87+
assertThat(result).isEmpty();
88+
}
89+
}
90+
91+
@Nested
92+
class GetScannedPackages {
93+
@Test
94+
@DisplayName("Should return the packages provided at construction")
95+
void shouldReturnPackagesProvidedAtConstruction() {
96+
// given
97+
var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE, OTHER_PACKAGE);
98+
99+
// when
100+
var result = scanner.getScannedPackages();
101+
102+
// then
103+
assertThat(result).containsExactlyInAnyOrder(FIXTURES_PACKAGE, OTHER_PACKAGE);
104+
}
105+
}
106+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;
2+
3+
public abstract class ScanTestAbstractImpl implements ScanTestInterface {
4+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;
2+
3+
@ScanTestAnnotation
4+
public class ScanTestAnnotatedClass {
5+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Target(ElementType.TYPE)
9+
@Retention(RetentionPolicy.RUNTIME)
10+
public @interface ScanTestAnnotation {
11+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;
2+
3+
public class ScanTestConcreteImpl implements ScanTestInterface {
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;
2+
3+
public interface ScanTestInterface {
4+
}

0 commit comments

Comments
 (0)