Skip to content

Commit 3566b9e

Browse files
use a more light-weight method for class scanning (#67)
* use a more light-weight method for class scanning * add missing annotations * fix test nesting * 2.4.5-RC1 * remove now obsolete depencency for class scanner --------- Co-authored-by: AboutBits Tech <tech@aboutbits.it>
1 parent 5000993 commit 3566b9e

8 files changed

Lines changed: 185 additions & 36 deletions

File tree

pom.xml

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
34
<modelVersion>4.0.0</modelVersion>
45

56
<parent>
@@ -11,7 +12,7 @@
1112

1213
<groupId>it.aboutbits</groupId>
1314
<artifactId>spring-boot-toolbox</artifactId>
14-
<version>2.4.4</version>
15+
<version>2.4.5-RC1</version>
1516
<description>Utility library for Spring Boot projects.</description>
1617
<packaging>jar</packaging>
1718

@@ -69,15 +70,6 @@
6970
<optional>true</optional>
7071
</dependency>
7172

72-
<!-- used to scan the classpath -->
73-
<!-- https://mvnrepository.com/artifact/io.github.classgraph/classgraph -->
74-
<dependency>
75-
<groupId>io.github.classgraph</groupId>
76-
<artifactId>classgraph</artifactId>
77-
<version>4.8.184</version>
78-
</dependency>
79-
80-
8173
<!-- Validation -->
8274
<!-- https://mvnrepository.com/artifact/commons-validator/commons-validator -->
8375
<dependency>
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: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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 ClassScanner {
22+
@Nested
23+
class GetSubTypesOf {
24+
@Test
25+
@DisplayName("Should find concrete and abstract subtypes of an interface")
26+
void shouldFindConcreteAndAbstractSubtypesOfInterface() {
27+
// given
28+
var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE);
29+
30+
// when
31+
var result = scanner.getSubTypesOf(ScanTestInterface.class);
32+
33+
// then
34+
assertThat(result).containsExactlyInAnyOrder(ScanTestConcreteImpl.class, ScanTestAbstractImpl.class);
35+
}
36+
37+
@Test
38+
@DisplayName("Should not include the target interface itself")
39+
void shouldNotIncludeTargetInterfaceItself() {
40+
// given
41+
var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE);
42+
43+
// when
44+
var result = scanner.getSubTypesOf(ScanTestInterface.class);
45+
46+
// then
47+
assertThat(result).doesNotContain(ScanTestInterface.class);
48+
}
49+
50+
@Test
51+
@DisplayName("Should return empty set when no subtypes exist in scanned package")
52+
void shouldReturnEmptySetWhenNoSubtypesExistInScannedPackage() {
53+
// given
54+
var scanner = ClassScannerUtil.getScannerForPackages(OTHER_PACKAGE);
55+
56+
// when
57+
var result = scanner.getSubTypesOf(ScanTestInterface.class);
58+
59+
// then
60+
assertThat(result).isEmpty();
61+
}
62+
}
63+
64+
@Nested
65+
class GetClassesAnnotatedWith {
66+
@Test
67+
@DisplayName("Should find classes annotated with the given annotation")
68+
void shouldFindClassesAnnotatedWithGivenAnnotation() {
69+
// given
70+
var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE);
71+
72+
// when
73+
var result = scanner.getClassesAnnotatedWith(ScanTestAnnotation.class);
74+
75+
// then
76+
assertThat(result).containsExactly(ScanTestAnnotatedClass.class);
77+
}
78+
79+
@Test
80+
@DisplayName("Should return empty set when no annotated classes exist in scanned package")
81+
void shouldReturnEmptySetWhenNoAnnotatedClassesExistInScannedPackage() {
82+
// given
83+
var scanner = ClassScannerUtil.getScannerForPackages(OTHER_PACKAGE);
84+
85+
// when
86+
var result = scanner.getClassesAnnotatedWith(ScanTestAnnotation.class);
87+
88+
// then
89+
assertThat(result).isEmpty();
90+
}
91+
}
92+
93+
@Nested
94+
class GetScannedPackages {
95+
@Test
96+
@DisplayName("Should return the packages provided at construction")
97+
void shouldReturnPackagesProvidedAtConstruction() {
98+
// given
99+
var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE, OTHER_PACKAGE);
100+
101+
// when
102+
var result = scanner.getScannedPackages();
103+
104+
// then
105+
assertThat(result).containsExactlyInAnyOrder(FIXTURES_PACKAGE, OTHER_PACKAGE);
106+
}
107+
}
108+
}
109+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;
2+
3+
import org.jspecify.annotations.NullMarked;
4+
5+
@NullMarked
6+
public abstract class ScanTestAbstractImpl implements ScanTestInterface {
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;
2+
3+
import org.jspecify.annotations.NullMarked;
4+
5+
@ScanTestAnnotation
6+
@NullMarked
7+
public class ScanTestAnnotatedClass {
8+
}
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;
2+
3+
import org.jspecify.annotations.NullMarked;
4+
5+
@NullMarked
6+
public class ScanTestConcreteImpl implements ScanTestInterface {
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;
2+
3+
import org.jspecify.annotations.NullMarked;
4+
5+
@NullMarked
6+
public interface ScanTestInterface {
7+
}

0 commit comments

Comments
 (0)