Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 3 additions & 11 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<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">
<modelVersion>4.0.0</modelVersion>

<parent>
Expand All @@ -11,7 +12,7 @@

<groupId>it.aboutbits</groupId>
<artifactId>spring-boot-toolbox</artifactId>
<version>2.4.4</version>
<version>2.4.5-RC1</version>
<description>Utility library for Spring Boot projects.</description>
<packaging>jar</packaging>

Expand Down Expand Up @@ -69,15 +70,6 @@
<optional>true</optional>
</dependency>

<!-- used to scan the classpath -->
<!-- https://mvnrepository.com/artifact/io.github.classgraph/classgraph -->
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<version>4.8.184</version>
</dependency>


<!-- Validation -->
<!-- https://mvnrepository.com/artifact/commons-validator/commons-validator -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, ClassScanner> 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() {
Expand All @@ -44,20 +36,36 @@ public String[] getScannedPackages() {

@SuppressWarnings("unchecked")
public <T> Set<Class<? extends T>> getSubTypesOf(Class<T> clazz) {
var classInfoList = clazz.isInterface()
? scanResult.getClassesImplementing(clazz)
: scanResult.getSubclasses(clazz);
return classInfoList.loadClasses()
.stream()
.map(item -> (Class<? extends T>) item)
var scanner = createScanner();
scanner.addIncludeFilter(new AssignableTypeFilter(clazz));
return Arrays.stream(packages)
.flatMap(pkg -> scanner.findCandidateComponents(pkg).stream())
.map(bd -> (Class<? extends T>) loadClass(Objects.requireNonNull(bd.getBeanClassName())))
.filter(c -> !c.equals(clazz))
.collect(Collectors.toSet());
}

public Set<Class<?>> getClassesAnnotatedWith(Class<? extends Annotation> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;

import org.jspecify.annotations.NullMarked;

@NullMarked
public abstract class ScanTestAbstractImpl implements ScanTestInterface {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;

import org.jspecify.annotations.NullMarked;

@ScanTestAnnotation
@NullMarked
public class ScanTestAnnotatedClass {
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;

import org.jspecify.annotations.NullMarked;

@NullMarked
public class ScanTestConcreteImpl implements ScanTestInterface {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package it.aboutbits.springboot.toolbox.reflection.util.testfixtures;

import org.jspecify.annotations.NullMarked;

@NullMarked
public interface ScanTestInterface {
}