diff --git a/pom.xml b/pom.xml index 14a4b51..6d6e5ed 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -11,7 +12,7 @@ it.aboutbits spring-boot-toolbox - 2.4.4 + 2.4.5-RC1 Utility library for Spring Boot projects. jar @@ -69,15 +70,6 @@ true - - - - io.github.classgraph - classgraph - 4.8.184 - - - diff --git a/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java b/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java index 195feb7..ba4b22d 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java @@ -1,41 +1,33 @@ package it.aboutbits.springboot.toolbox.reflection.util; -import io.github.classgraph.ClassGraph; -import io.github.classgraph.ClassInfo; -import io.github.classgraph.ScanResult; +import lombok.SneakyThrows; import org.jspecify.annotations.NullMarked; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; import java.lang.annotation.Annotation; import java.util.Arrays; -import java.util.Map; +import java.util.Objects; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @NullMarked public final class ClassScannerUtil { - private static final Map CACHE = new ConcurrentHashMap<>(); private ClassScannerUtil() { } public static ClassScanner getScannerForPackages(String... packages) { - var cacheKey = Arrays.stream(packages) - .sorted() - .collect(Collectors.joining("|")); - return CACHE.computeIfAbsent(cacheKey, _ -> new ClassScanner(packages)); + return new ClassScanner(packages); } public static final class ClassScanner { - private final ScanResult scanResult; private final String[] packages; private ClassScanner(String... packages) { this.packages = packages; - this.scanResult = new ClassGraph() - .enableAllInfo() - .acceptPackages(packages) - .scan(); } public String[] getScannedPackages() { @@ -44,20 +36,36 @@ public String[] getScannedPackages() { @SuppressWarnings("unchecked") public Set> getSubTypesOf(Class clazz) { - var classInfoList = clazz.isInterface() - ? scanResult.getClassesImplementing(clazz) - : scanResult.getSubclasses(clazz); - return classInfoList.loadClasses() - .stream() - .map(item -> (Class) item) + var scanner = createScanner(); + scanner.addIncludeFilter(new AssignableTypeFilter(clazz)); + return Arrays.stream(packages) + .flatMap(pkg -> scanner.findCandidateComponents(pkg).stream()) + .map(bd -> (Class) loadClass(Objects.requireNonNull(bd.getBeanClassName()))) + .filter(c -> !c.equals(clazz)) .collect(Collectors.toSet()); } public Set> getClassesAnnotatedWith(Class clazz) { - var result = scanResult.getClassesWithAnnotation(clazz); - return result.stream().map( - ClassInfo::loadClass - ).collect(Collectors.toSet()); + var scanner = createScanner(); + scanner.addIncludeFilter(new AnnotationTypeFilter(clazz)); + return Arrays.stream(packages) + .flatMap(pkg -> scanner.findCandidateComponents(pkg).stream()) + .map(bd -> loadClass(Objects.requireNonNull(bd.getBeanClassName()))) + .collect(Collectors.toSet()); + } + + private static ClassPathScanningCandidateComponentProvider createScanner() { + return new ClassPathScanningCandidateComponentProvider(false) { + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + return true; + } + }; + } + + @SneakyThrows(ClassNotFoundException.class) + private static Class loadClass(String className) { + return Class.forName(className); } } } diff --git a/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtilTest.java b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtilTest.java new file mode 100644 index 0000000..43894da --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtilTest.java @@ -0,0 +1,109 @@ +package it.aboutbits.springboot.toolbox.reflection.util; + +import it.aboutbits.springboot.toolbox.reflection.util.testfixtures.ScanTestAbstractImpl; +import it.aboutbits.springboot.toolbox.reflection.util.testfixtures.ScanTestAnnotatedClass; +import it.aboutbits.springboot.toolbox.reflection.util.testfixtures.ScanTestAnnotation; +import it.aboutbits.springboot.toolbox.reflection.util.testfixtures.ScanTestConcreteImpl; +import it.aboutbits.springboot.toolbox.reflection.util.testfixtures.ScanTestInterface; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@NullMarked +class ClassScannerUtilTest { + private static final String FIXTURES_PACKAGE = "it.aboutbits.springboot.toolbox.reflection.util.testfixtures"; + private static final String OTHER_PACKAGE = "it.aboutbits.springboot.toolbox.util"; + + @Nested + class ClassScanner { + @Nested + class GetSubTypesOf { + @Test + @DisplayName("Should find concrete and abstract subtypes of an interface") + void shouldFindConcreteAndAbstractSubtypesOfInterface() { + // given + var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE); + + // when + var result = scanner.getSubTypesOf(ScanTestInterface.class); + + // then + assertThat(result).containsExactlyInAnyOrder(ScanTestConcreteImpl.class, ScanTestAbstractImpl.class); + } + + @Test + @DisplayName("Should not include the target interface itself") + void shouldNotIncludeTargetInterfaceItself() { + // given + var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE); + + // when + var result = scanner.getSubTypesOf(ScanTestInterface.class); + + // then + assertThat(result).doesNotContain(ScanTestInterface.class); + } + + @Test + @DisplayName("Should return empty set when no subtypes exist in scanned package") + void shouldReturnEmptySetWhenNoSubtypesExistInScannedPackage() { + // given + var scanner = ClassScannerUtil.getScannerForPackages(OTHER_PACKAGE); + + // when + var result = scanner.getSubTypesOf(ScanTestInterface.class); + + // then + assertThat(result).isEmpty(); + } + } + + @Nested + class GetClassesAnnotatedWith { + @Test + @DisplayName("Should find classes annotated with the given annotation") + void shouldFindClassesAnnotatedWithGivenAnnotation() { + // given + var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE); + + // when + var result = scanner.getClassesAnnotatedWith(ScanTestAnnotation.class); + + // then + assertThat(result).containsExactly(ScanTestAnnotatedClass.class); + } + + @Test + @DisplayName("Should return empty set when no annotated classes exist in scanned package") + void shouldReturnEmptySetWhenNoAnnotatedClassesExistInScannedPackage() { + // given + var scanner = ClassScannerUtil.getScannerForPackages(OTHER_PACKAGE); + + // when + var result = scanner.getClassesAnnotatedWith(ScanTestAnnotation.class); + + // then + assertThat(result).isEmpty(); + } + } + + @Nested + class GetScannedPackages { + @Test + @DisplayName("Should return the packages provided at construction") + void shouldReturnPackagesProvidedAtConstruction() { + // given + var scanner = ClassScannerUtil.getScannerForPackages(FIXTURES_PACKAGE, OTHER_PACKAGE); + + // when + var result = scanner.getScannedPackages(); + + // then + assertThat(result).containsExactlyInAnyOrder(FIXTURES_PACKAGE, OTHER_PACKAGE); + } + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestAbstractImpl.java b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestAbstractImpl.java new file mode 100644 index 0000000..17d8a84 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestAbstractImpl.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.toolbox.reflection.util.testfixtures; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public abstract class ScanTestAbstractImpl implements ScanTestInterface { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestAnnotatedClass.java b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestAnnotatedClass.java new file mode 100644 index 0000000..a6d8fbc --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestAnnotatedClass.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.reflection.util.testfixtures; + +import org.jspecify.annotations.NullMarked; + +@ScanTestAnnotation +@NullMarked +public class ScanTestAnnotatedClass { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestAnnotation.java b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestAnnotation.java new file mode 100644 index 0000000..765b24c --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestAnnotation.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.reflection.util.testfixtures; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ScanTestAnnotation { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestConcreteImpl.java b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestConcreteImpl.java new file mode 100644 index 0000000..ae29125 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestConcreteImpl.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.toolbox.reflection.util.testfixtures; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class ScanTestConcreteImpl implements ScanTestInterface { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestInterface.java b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestInterface.java new file mode 100644 index 0000000..fe061b9 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/reflection/util/testfixtures/ScanTestInterface.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.toolbox.reflection.util.testfixtures; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public interface ScanTestInterface { +}